mirror of
https://github.com/lennart-k/rustical.git
synced 2025-12-13 21:42:34 +00:00
carddav: Implement DAV Push
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -2884,6 +2884,7 @@ name = "rustical_store_sqlite"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
|
"derive_more 1.0.0",
|
||||||
"rustical_store",
|
"rustical_store",
|
||||||
"serde",
|
"serde",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
|
|||||||
@@ -2,35 +2,13 @@ use crate::Error;
|
|||||||
use actix_web::http::header;
|
use actix_web::http::header;
|
||||||
use actix_web::web::{Data, Path};
|
use actix_web::web::{Data, Path};
|
||||||
use actix_web::{HttpRequest, HttpResponse};
|
use actix_web::{HttpRequest, HttpResponse};
|
||||||
|
use rustical_dav::push::PushRegister;
|
||||||
use rustical_store::auth::User;
|
use rustical_store::auth::User;
|
||||||
use rustical_store::{CalendarStore, Subscription, SubscriptionStore};
|
use rustical_store::{CalendarStore, Subscription, SubscriptionStore};
|
||||||
use rustical_xml::{XmlDeserialize, XmlDocument, XmlRootTag};
|
use rustical_xml::XmlDocument;
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
use tracing_actix_web::RootSpan;
|
use tracing_actix_web::RootSpan;
|
||||||
|
|
||||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
|
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_DAVPUSH")]
|
|
||||||
struct WebPushSubscription {
|
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_DAVPUSH")]
|
|
||||||
push_resource: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
|
|
||||||
struct SubscriptionElement {
|
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_DAVPUSH")]
|
|
||||||
pub web_push_subscription: WebPushSubscription,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(XmlDeserialize, XmlRootTag, Clone, Debug, PartialEq)]
|
|
||||||
#[xml(root = b"push-register")]
|
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_DAVPUSH")]
|
|
||||||
struct PushRegister {
|
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_DAVPUSH")]
|
|
||||||
subscription: SubscriptionElement,
|
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_DAVPUSH")]
|
|
||||||
expires: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[instrument(parent = root_span.id(), skip(store, subscription_store, root_span, req))]
|
#[instrument(parent = root_span.id(), skip(store, subscription_store, root_span, req))]
|
||||||
pub async fn route_post<C: CalendarStore + ?Sized, S: SubscriptionStore + ?Sized>(
|
pub async fn route_post<C: CalendarStore + ?Sized, S: SubscriptionStore + ?Sized>(
|
||||||
path: Path<(String, String)>,
|
path: Path<(String, String)>,
|
||||||
@@ -79,37 +57,3 @@ pub async fn route_post<C: CalendarStore + ?Sized, S: SubscriptionStore + ?Sized
|
|||||||
.append_header((header::EXPIRES, expires.to_rfc2822()))
|
.append_header((header::EXPIRES, expires.to_rfc2822()))
|
||||||
.finish())
|
.finish())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_xml_push_register() {
|
|
||||||
let push_register = PushRegister::parse_str(
|
|
||||||
r#"
|
|
||||||
<?xml version="1.0" encoding="utf-8" ?>
|
|
||||||
<push-register xmlns="https://bitfire.at/webdav-push">
|
|
||||||
<subscription>
|
|
||||||
<web-push-subscription>
|
|
||||||
<push-resource>https://up.example.net/yohd4yai5Phiz1wi</push-resource>
|
|
||||||
</web-push-subscription>
|
|
||||||
</subscription>
|
|
||||||
<expires>Wed, 20 Dec 2023 10:03:31 GMT</expires>
|
|
||||||
</push-register>
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
push_register,
|
|
||||||
PushRegister {
|
|
||||||
subscription: SubscriptionElement {
|
|
||||||
web_push_subscription: WebPushSubscription {
|
|
||||||
push_resource: "https://up.example.net/yohd4yai5Phiz1wi".to_owned()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
expires: Some("Wed, 20 Dec 2023 10:03:31 GMT".to_owned())
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -80,24 +80,3 @@ impl Default for SupportedReportSet {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, XmlSerialize, PartialEq)]
|
|
||||||
pub enum Transport {
|
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_DAVPUSH")]
|
|
||||||
WebPush,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, XmlSerialize, PartialEq)]
|
|
||||||
pub struct Transports {
|
|
||||||
#[xml(flatten, ty = "untagged")]
|
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_DAVPUSH")]
|
|
||||||
transports: Vec<Transport>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Transports {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
transports: vec![Transport::WebPush],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
use super::methods::mkcalendar::route_mkcalendar;
|
use super::methods::mkcalendar::route_mkcalendar;
|
||||||
use super::methods::post::route_post;
|
use super::methods::post::route_post;
|
||||||
use super::methods::report::route_report_calendar;
|
use super::methods::report::route_report_calendar;
|
||||||
use super::prop::{
|
use super::prop::{SupportedCalendarComponentSet, SupportedCalendarData, SupportedReportSet};
|
||||||
SupportedCalendarComponentSet, SupportedCalendarData, SupportedReportSet, Transports,
|
|
||||||
};
|
|
||||||
use crate::calendar_object::resource::CalendarObjectResource;
|
use crate::calendar_object::resource::CalendarObjectResource;
|
||||||
use crate::principal::PrincipalResource;
|
use crate::principal::PrincipalResource;
|
||||||
use crate::Error;
|
use crate::Error;
|
||||||
@@ -13,6 +11,7 @@ use actix_web::web;
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use derive_more::derive::{From, Into};
|
use derive_more::derive::{From, Into};
|
||||||
use rustical_dav::privileges::UserPrivilegeSet;
|
use rustical_dav::privileges::UserPrivilegeSet;
|
||||||
|
use rustical_dav::push::Transports;
|
||||||
use rustical_dav::resource::{Resource, ResourceService};
|
use rustical_dav::resource::{Resource, ResourceService};
|
||||||
use rustical_dav::xml::{HrefElement, Resourcetype, ResourcetypeInner};
|
use rustical_dav::xml::{HrefElement, Resourcetype, ResourcetypeInner};
|
||||||
use rustical_store::auth::User;
|
use rustical_store::auth::User;
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
pub mod mkcol;
|
pub mod mkcol;
|
||||||
|
pub mod post;
|
||||||
pub mod report;
|
pub mod report;
|
||||||
|
|||||||
59
crates/carddav/src/addressbook/methods/post.rs
Normal file
59
crates/carddav/src/addressbook/methods/post.rs
Normal 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())
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
use super::methods::mkcol::route_mkcol;
|
use super::methods::mkcol::route_mkcol;
|
||||||
|
use super::methods::post::route_post;
|
||||||
use super::methods::report::route_report_addressbook;
|
use super::methods::report::route_report_addressbook;
|
||||||
use super::prop::{SupportedAddressData, SupportedReportSet};
|
use super::prop::{SupportedAddressData, SupportedReportSet};
|
||||||
use crate::address_object::resource::AddressObjectResource;
|
use crate::address_object::resource::AddressObjectResource;
|
||||||
@@ -10,22 +11,29 @@ use actix_web::web;
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use derive_more::derive::{From, Into};
|
use derive_more::derive::{From, Into};
|
||||||
use rustical_dav::privileges::UserPrivilegeSet;
|
use rustical_dav::privileges::UserPrivilegeSet;
|
||||||
|
use rustical_dav::push::Transports;
|
||||||
use rustical_dav::resource::{Resource, ResourceService};
|
use rustical_dav::resource::{Resource, ResourceService};
|
||||||
use rustical_dav::xml::{Resourcetype, ResourcetypeInner};
|
use rustical_dav::xml::{Resourcetype, ResourcetypeInner};
|
||||||
use rustical_store::auth::User;
|
use rustical_store::auth::User;
|
||||||
use rustical_store::{Addressbook, AddressbookStore};
|
use rustical_store::{Addressbook, AddressbookStore, SubscriptionStore};
|
||||||
use rustical_xml::{XmlDeserialize, XmlSerialize};
|
use rustical_xml::{XmlDeserialize, XmlSerialize};
|
||||||
|
use std::marker::PhantomData;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use strum::{EnumDiscriminants, EnumString, IntoStaticStr, VariantNames};
|
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>,
|
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 {
|
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)]
|
#[xml(ns = "rustical_dav::namespace::NS_DAV", skip_deserializing)]
|
||||||
Getcontenttype(&'static str),
|
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)
|
// CardDAV (RFC 6352)
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV")]
|
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV")]
|
||||||
AddressbookDescription(Option<String>),
|
AddressbookDescription(Option<String>),
|
||||||
@@ -90,6 +105,8 @@ impl Resource for AddressbookResource {
|
|||||||
AddressbookPropName::Getcontenttype => {
|
AddressbookPropName::Getcontenttype => {
|
||||||
AddressbookProp::Getcontenttype("text/vcard;charset=utf-8")
|
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::MaxResourceSize => AddressbookProp::MaxResourceSize(10000000),
|
||||||
AddressbookPropName::SupportedReportSet => {
|
AddressbookPropName::SupportedReportSet => {
|
||||||
AddressbookProp::SupportedReportSet(SupportedReportSet::default())
|
AddressbookProp::SupportedReportSet(SupportedReportSet::default())
|
||||||
@@ -116,6 +133,8 @@ impl Resource for AddressbookResource {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
AddressbookProp::Getcontenttype(_) => Err(rustical_dav::Error::PropReadOnly),
|
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::MaxResourceSize(_) => Err(rustical_dav::Error::PropReadOnly),
|
||||||
AddressbookProp::SupportedReportSet(_) => Err(rustical_dav::Error::PropReadOnly),
|
AddressbookProp::SupportedReportSet(_) => Err(rustical_dav::Error::PropReadOnly),
|
||||||
AddressbookProp::SupportedAddressData(_) => Err(rustical_dav::Error::PropReadOnly),
|
AddressbookProp::SupportedAddressData(_) => Err(rustical_dav::Error::PropReadOnly),
|
||||||
@@ -135,6 +154,8 @@ impl Resource for AddressbookResource {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
AddressbookPropName::Getcontenttype => Err(rustical_dav::Error::PropReadOnly),
|
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::MaxResourceSize => Err(rustical_dav::Error::PropReadOnly),
|
||||||
AddressbookPropName::SupportedReportSet => Err(rustical_dav::Error::PropReadOnly),
|
AddressbookPropName::SupportedReportSet => Err(rustical_dav::Error::PropReadOnly),
|
||||||
AddressbookPropName::SupportedAddressData => Err(rustical_dav::Error::PropReadOnly),
|
AddressbookPropName::SupportedAddressData => Err(rustical_dav::Error::PropReadOnly),
|
||||||
@@ -153,7 +174,9 @@ impl Resource for AddressbookResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait(?Send)]
|
#[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 MemberType = AddressObjectResource;
|
||||||
type PathComponents = (String, String); // principal, addressbook_id
|
type PathComponents = (String, String); // principal, addressbook_id
|
||||||
type Resource = AddressbookResource;
|
type Resource = AddressbookResource;
|
||||||
@@ -220,5 +243,6 @@ impl<AS: AddressbookStore + ?Sized> ResourceService for AddressbookResourceServi
|
|||||||
let report_method = web::method(Method::from_str("REPORT").unwrap());
|
let report_method = web::method(Method::from_str("REPORT").unwrap());
|
||||||
res.route(mkcol_method.to(route_mkcol::<AS>))
|
res.route(mkcol_method.to(route_mkcol::<AS>))
|
||||||
.route(report_method.to(route_report_addressbook::<AS>))
|
.route(report_method.to(route_report_addressbook::<AS>))
|
||||||
|
.post(route_post::<AS, S>)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ use rustical_dav::resource::{NamedRoute, ResourceService};
|
|||||||
use rustical_dav::resources::RootResourceService;
|
use rustical_dav::resources::RootResourceService;
|
||||||
use rustical_store::{
|
use rustical_store::{
|
||||||
auth::{AuthenticationMiddleware, AuthenticationProvider},
|
auth::{AuthenticationMiddleware, AuthenticationProvider},
|
||||||
AddressbookStore,
|
AddressbookStore, SubscriptionStore,
|
||||||
};
|
};
|
||||||
use std::sync::Arc;
|
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());
|
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,
|
cfg: &mut web::ServiceConfig,
|
||||||
auth_provider: Arc<AP>,
|
auth_provider: Arc<AP>,
|
||||||
store: Arc<A>,
|
store: Arc<A>,
|
||||||
|
subscription_store: Arc<S>,
|
||||||
) {
|
) {
|
||||||
cfg.service(
|
cfg.service(
|
||||||
web::scope("")
|
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(store.clone()))
|
||||||
|
.app_data(Data::from(subscription_store))
|
||||||
.service(RootResourceService::<PrincipalResource>::default().actix_resource())
|
.service(RootResourceService::<PrincipalResource>::default().actix_resource())
|
||||||
.service(
|
.service(
|
||||||
web::scope("/user").service(
|
web::scope("/user").service(
|
||||||
@@ -70,7 +76,7 @@ pub fn configure_dav<AP: AuthenticationProvider, A: AddressbookStore + ?Sized>(
|
|||||||
.service(
|
.service(
|
||||||
web::scope("/{addressbook}")
|
web::scope("/{addressbook}")
|
||||||
.service(
|
.service(
|
||||||
AddressbookResourceService::<A>::new(store.clone())
|
AddressbookResourceService::<A, S>::new(store.clone())
|
||||||
.actix_resource(),
|
.actix_resource(),
|
||||||
)
|
)
|
||||||
.service(
|
.service(
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ pub mod depth_header;
|
|||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod namespace;
|
pub mod namespace;
|
||||||
pub mod privileges;
|
pub mod privileges;
|
||||||
|
pub mod push;
|
||||||
pub mod resource;
|
pub mod resource;
|
||||||
pub mod resources;
|
pub mod resources;
|
||||||
pub mod xml;
|
pub mod xml;
|
||||||
|
|||||||
80
crates/dav/src/push/mod.rs
Normal file
80
crates/dav/src/push/mod.rs
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
use rustical_xml::{XmlDeserialize, XmlRootTag, XmlSerialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, XmlSerialize, PartialEq)]
|
||||||
|
pub enum Transport {
|
||||||
|
#[xml(ns = "crate::namespace::NS_DAVPUSH")]
|
||||||
|
WebPush,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, XmlSerialize, PartialEq)]
|
||||||
|
pub struct Transports {
|
||||||
|
#[xml(flatten, ty = "untagged")]
|
||||||
|
#[xml(ns = "crate::namespace::NS_DAVPUSH")]
|
||||||
|
transports: Vec<Transport>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Transports {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
transports: vec![Transport::WebPush],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
|
||||||
|
#[xml(ns = "crate::namespace::NS_DAVPUSH")]
|
||||||
|
pub struct WebPushSubscription {
|
||||||
|
#[xml(ns = "crate::namespace::NS_DAVPUSH")]
|
||||||
|
pub push_resource: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
|
||||||
|
pub struct SubscriptionElement {
|
||||||
|
#[xml(ns = "crate::namespace::NS_DAVPUSH")]
|
||||||
|
pub web_push_subscription: WebPushSubscription,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(XmlDeserialize, XmlRootTag, Clone, Debug, PartialEq)]
|
||||||
|
#[xml(root = b"push-register")]
|
||||||
|
#[xml(ns = "crate::namespace::NS_DAVPUSH")]
|
||||||
|
pub struct PushRegister {
|
||||||
|
#[xml(ns = "crate::namespace::NS_DAVPUSH")]
|
||||||
|
pub subscription: SubscriptionElement,
|
||||||
|
#[xml(ns = "crate::namespace::NS_DAVPUSH")]
|
||||||
|
pub expires: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use rustical_xml::XmlDocument;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_xml_push_register() {
|
||||||
|
let push_register = PushRegister::parse_str(
|
||||||
|
r#"
|
||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<push-register xmlns="https://bitfire.at/webdav-push">
|
||||||
|
<subscription>
|
||||||
|
<web-push-subscription>
|
||||||
|
<push-resource>https://up.example.net/yohd4yai5Phiz1wi</push-resource>
|
||||||
|
</web-push-subscription>
|
||||||
|
</subscription>
|
||||||
|
<expires>Wed, 20 Dec 2023 10:03:31 GMT</expires>
|
||||||
|
</push-register>
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
push_register,
|
||||||
|
PushRegister {
|
||||||
|
subscription: SubscriptionElement {
|
||||||
|
web_push_subscription: WebPushSubscription {
|
||||||
|
push_resource: "https://up.example.net/yohd4yai5Phiz1wi".to_owned()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
expires: Some("Wed, 20 Dec 2023 10:03:31 GMT".to_owned())
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,3 +14,4 @@ serde = { workspace = true }
|
|||||||
sqlx = { workspace = true }
|
sqlx = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
|
derive_more.workspace = true
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
use super::{ChangeOperation, SqliteStore};
|
use super::ChangeOperation;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use rustical_store::{AddressObject, Addressbook, AddressbookStore};
|
use derive_more::derive::Constructor;
|
||||||
use sqlx::{Sqlite, Transaction};
|
use rustical_store::{
|
||||||
|
synctoken::format_synctoken, AddressObject, Addressbook, AddressbookStore, CollectionOperation,
|
||||||
|
CollectionOperationDomain, CollectionOperationType, Error,
|
||||||
|
};
|
||||||
|
use sqlx::{Sqlite, SqlitePool, Transaction};
|
||||||
|
use tokio::sync::mpsc::Sender;
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -25,16 +30,21 @@ async fn log_object_operation(
|
|||||||
addressbook_id: &str,
|
addressbook_id: &str,
|
||||||
object_id: &str,
|
object_id: &str,
|
||||||
operation: ChangeOperation,
|
operation: ChangeOperation,
|
||||||
) -> Result<(), sqlx::Error> {
|
) -> Result<String, sqlx::Error> {
|
||||||
sqlx::query!(
|
struct Synctoken {
|
||||||
|
synctoken: i64,
|
||||||
|
}
|
||||||
|
let Synctoken { synctoken } = sqlx::query_as!(
|
||||||
|
Synctoken,
|
||||||
r#"
|
r#"
|
||||||
UPDATE addressbooks
|
UPDATE addressbooks
|
||||||
SET synctoken = synctoken + 1
|
SET synctoken = synctoken + 1
|
||||||
WHERE (principal, id) = (?1, ?2)"#,
|
WHERE (principal, id) = (?1, ?2)
|
||||||
|
RETURNING synctoken"#,
|
||||||
principal,
|
principal,
|
||||||
addressbook_id
|
addressbook_id
|
||||||
)
|
)
|
||||||
.execute(&mut **tx)
|
.fetch_one(&mut **tx)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
@@ -50,11 +60,17 @@ async fn log_object_operation(
|
|||||||
)
|
)
|
||||||
.execute(&mut **tx)
|
.execute(&mut **tx)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(())
|
Ok(format_synctoken(synctoken))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Constructor)]
|
||||||
|
pub struct SqliteAddressbookStore {
|
||||||
|
db: SqlitePool,
|
||||||
|
sender: Sender<CollectionOperation>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl AddressbookStore for SqliteStore {
|
impl AddressbookStore for SqliteAddressbookStore {
|
||||||
#[instrument]
|
#[instrument]
|
||||||
async fn get_addressbook(
|
async fn get_addressbook(
|
||||||
&self,
|
&self,
|
||||||
@@ -165,6 +181,12 @@ impl AddressbookStore for SqliteStore {
|
|||||||
addressbook_id: &str,
|
addressbook_id: &str,
|
||||||
use_trashbin: bool,
|
use_trashbin: bool,
|
||||||
) -> Result<(), rustical_store::Error> {
|
) -> Result<(), rustical_store::Error> {
|
||||||
|
let addressbook = match self.get_addressbook(principal, addressbook_id).await {
|
||||||
|
Ok(addressbook) => Some(addressbook),
|
||||||
|
Err(Error::NotFound) => None,
|
||||||
|
Err(err) => return Err(err),
|
||||||
|
};
|
||||||
|
|
||||||
match use_trashbin {
|
match use_trashbin {
|
||||||
true => {
|
true => {
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
@@ -185,6 +207,17 @@ impl AddressbookStore for SqliteStore {
|
|||||||
.map_err(crate::Error::from)?;
|
.map_err(crate::Error::from)?;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if let Some(addressbook) = addressbook {
|
||||||
|
// TODO: Watch for errors here?
|
||||||
|
let _ = self.sender.try_send(CollectionOperation {
|
||||||
|
r#type: CollectionOperationType::Delete,
|
||||||
|
domain: CollectionOperationDomain::Addressbook,
|
||||||
|
topic: addressbook.push_topic,
|
||||||
|
sync_token: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -320,7 +353,7 @@ impl AddressbookStore for SqliteStore {
|
|||||||
.await
|
.await
|
||||||
.map_err(crate::Error::from)?;
|
.map_err(crate::Error::from)?;
|
||||||
|
|
||||||
log_object_operation(
|
let synctoken = log_object_operation(
|
||||||
&mut tx,
|
&mut tx,
|
||||||
&principal,
|
&principal,
|
||||||
&addressbook_id,
|
&addressbook_id,
|
||||||
@@ -330,6 +363,17 @@ impl AddressbookStore for SqliteStore {
|
|||||||
.await
|
.await
|
||||||
.map_err(crate::Error::from)?;
|
.map_err(crate::Error::from)?;
|
||||||
|
|
||||||
|
// TODO: Watch for errors here?
|
||||||
|
let _ = self.sender.try_send(CollectionOperation {
|
||||||
|
r#type: CollectionOperationType::Object,
|
||||||
|
domain: CollectionOperationDomain::Addressbook,
|
||||||
|
topic: self
|
||||||
|
.get_addressbook(&principal, &addressbook_id)
|
||||||
|
.await?
|
||||||
|
.push_topic,
|
||||||
|
sync_token: Some(synctoken),
|
||||||
|
});
|
||||||
|
|
||||||
tx.commit().await.map_err(crate::Error::from)?;
|
tx.commit().await.map_err(crate::Error::from)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -366,7 +410,7 @@ impl AddressbookStore for SqliteStore {
|
|||||||
.map_err(crate::Error::from)?;
|
.map_err(crate::Error::from)?;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
log_object_operation(
|
let synctoken = log_object_operation(
|
||||||
&mut tx,
|
&mut tx,
|
||||||
principal,
|
principal,
|
||||||
addressbook_id,
|
addressbook_id,
|
||||||
@@ -375,7 +419,19 @@ impl AddressbookStore for SqliteStore {
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(crate::Error::from)?;
|
.map_err(crate::Error::from)?;
|
||||||
|
|
||||||
tx.commit().await.map_err(crate::Error::from)?;
|
tx.commit().await.map_err(crate::Error::from)?;
|
||||||
|
|
||||||
|
// TODO: Watch for errors here?
|
||||||
|
let _ = self.sender.try_send(CollectionOperation {
|
||||||
|
r#type: CollectionOperationType::Object,
|
||||||
|
domain: CollectionOperationDomain::Addressbook,
|
||||||
|
topic: self
|
||||||
|
.get_addressbook(principal, addressbook_id)
|
||||||
|
.await?
|
||||||
|
.push_topic,
|
||||||
|
sync_token: Some(synctoken),
|
||||||
|
});
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -397,7 +453,7 @@ impl AddressbookStore for SqliteStore {
|
|||||||
.execute(&mut *tx)
|
.execute(&mut *tx)
|
||||||
.await.map_err(crate::Error::from)?;
|
.await.map_err(crate::Error::from)?;
|
||||||
|
|
||||||
log_object_operation(
|
let synctoken = log_object_operation(
|
||||||
&mut tx,
|
&mut tx,
|
||||||
principal,
|
principal,
|
||||||
addressbook_id,
|
addressbook_id,
|
||||||
@@ -407,6 +463,18 @@ impl AddressbookStore for SqliteStore {
|
|||||||
.await
|
.await
|
||||||
.map_err(crate::Error::from)?;
|
.map_err(crate::Error::from)?;
|
||||||
tx.commit().await.map_err(crate::Error::from)?;
|
tx.commit().await.map_err(crate::Error::from)?;
|
||||||
|
|
||||||
|
// TODO: Watch for errors here?
|
||||||
|
let _ = self.sender.try_send(CollectionOperation {
|
||||||
|
r#type: CollectionOperationType::Object,
|
||||||
|
domain: CollectionOperationDomain::Addressbook,
|
||||||
|
topic: self
|
||||||
|
.get_addressbook(principal, addressbook_id)
|
||||||
|
.await?
|
||||||
|
.push_topic,
|
||||||
|
sync_token: Some(synctoken),
|
||||||
|
});
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use super::ChangeOperation;
|
use super::ChangeOperation;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use derive_more::derive::Constructor;
|
||||||
use rustical_store::synctoken::format_synctoken;
|
use rustical_store::synctoken::format_synctoken;
|
||||||
use rustical_store::{Calendar, CalendarObject, CalendarStore, Error};
|
use rustical_store::{Calendar, CalendarObject, CalendarStore, Error};
|
||||||
use rustical_store::{CollectionOperation, CollectionOperationType};
|
use rustical_store::{CollectionOperation, CollectionOperationType};
|
||||||
@@ -65,18 +66,12 @@ async fn log_object_operation(
|
|||||||
Ok(format_synctoken(synctoken))
|
Ok(format_synctoken(synctoken))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Constructor)]
|
||||||
pub struct SqliteCalendarStore {
|
pub struct SqliteCalendarStore {
|
||||||
db: SqlitePool,
|
db: SqlitePool,
|
||||||
sender: Sender<CollectionOperation>,
|
sender: Sender<CollectionOperation>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SqliteCalendarStore {
|
|
||||||
pub fn new(db: SqlitePool, sender: Sender<CollectionOperation>) -> Self {
|
|
||||||
Self { db, sender }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl CalendarStore for SqliteCalendarStore {
|
impl CalendarStore for SqliteCalendarStore {
|
||||||
#[instrument]
|
#[instrument]
|
||||||
|
|||||||
@@ -37,11 +37,16 @@ pub fn make_app<
|
|||||||
auth_provider.clone(),
|
auth_provider.clone(),
|
||||||
cal_store.clone(),
|
cal_store.clone(),
|
||||||
addr_store.clone(),
|
addr_store.clone(),
|
||||||
subscription_store,
|
subscription_store.clone(),
|
||||||
)
|
)
|
||||||
}))
|
}))
|
||||||
.service(web::scope("/carddav").configure(|cfg| {
|
.service(web::scope("/carddav").configure(|cfg| {
|
||||||
rustical_carddav::configure_dav(cfg, auth_provider.clone(), addr_store.clone())
|
rustical_carddav::configure_dav(
|
||||||
|
cfg,
|
||||||
|
auth_provider.clone(),
|
||||||
|
addr_store.clone(),
|
||||||
|
subscription_store,
|
||||||
|
)
|
||||||
}))
|
}))
|
||||||
.service(
|
.service(
|
||||||
web::scope("/.well-known")
|
web::scope("/.well-known")
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use config::{DataStoreConfig, SqliteDataStoreConfig};
|
|||||||
use rustical_dav::xml::multistatus::PropstatElement;
|
use rustical_dav::xml::multistatus::PropstatElement;
|
||||||
use rustical_store::auth::StaticUserStore;
|
use rustical_store::auth::StaticUserStore;
|
||||||
use rustical_store::{AddressbookStore, CalendarStore, CollectionOperation, SubscriptionStore};
|
use rustical_store::{AddressbookStore, CalendarStore, CollectionOperation, SubscriptionStore};
|
||||||
|
use rustical_store_sqlite::addressbook_store::SqliteAddressbookStore;
|
||||||
use rustical_store_sqlite::calendar_store::SqliteCalendarStore;
|
use rustical_store_sqlite::calendar_store::SqliteCalendarStore;
|
||||||
use rustical_store_sqlite::{create_db_pool, SqliteStore};
|
use rustical_store_sqlite::{create_db_pool, SqliteStore};
|
||||||
use rustical_xml::{XmlRootTag, XmlSerialize, XmlSerializeRoot};
|
use rustical_xml::{XmlRootTag, XmlSerialize, XmlSerializeRoot};
|
||||||
@@ -56,7 +57,7 @@ async fn get_data_stores(
|
|||||||
// Channel to watch for changes (for DAV Push)
|
// Channel to watch for changes (for DAV Push)
|
||||||
let (send, recv) = tokio::sync::mpsc::channel(1000);
|
let (send, recv) = tokio::sync::mpsc::channel(1000);
|
||||||
|
|
||||||
let addressbook_store = Arc::new(SqliteStore::new(db.clone()));
|
let addressbook_store = Arc::new(SqliteAddressbookStore::new(db.clone(), send.clone()));
|
||||||
let cal_store = Arc::new(SqliteCalendarStore::new(db.clone(), send));
|
let cal_store = Arc::new(SqliteCalendarStore::new(db.clone(), send));
|
||||||
let subscription_store = Arc::new(SqliteStore::new(db.clone()));
|
let subscription_store = Arc::new(SqliteStore::new(db.clone()));
|
||||||
(addressbook_store, cal_store, subscription_store, recv)
|
(addressbook_store, cal_store, subscription_store, recv)
|
||||||
@@ -107,7 +108,6 @@ async fn main() -> Result<()> {
|
|||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let subscription_store = subscription_store_clone.clone();
|
let subscription_store = subscription_store_clone.clone();
|
||||||
while let Some(message) = update_recv.recv().await {
|
while let Some(message) = update_recv.recv().await {
|
||||||
dbg!(&message);
|
|
||||||
if let Ok(subscribers) =
|
if let Ok(subscribers) =
|
||||||
subscription_store.get_subscriptions(&message.topic).await
|
subscription_store.get_subscriptions(&message.topic).await
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user