Dav Push: Logic to register subscriptions

This commit is contained in:
Lennart
2025-01-12 20:39:53 +01:00
parent 185eb8bddd
commit 5b418ec583
12 changed files with 261 additions and 15 deletions

View File

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

View File

@@ -0,0 +1,115 @@
use crate::Error;
use actix_web::http::header;
use actix_web::web::{Data, Path};
use actix_web::{HttpRequest, HttpResponse};
use rustical_store::auth::User;
use rustical_store::{CalendarStore, Subscription, SubscriptionStore};
use rustical_xml::{XmlDeserialize, XmlDocument, XmlRootTag};
use tracing::instrument;
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))]
pub async fn route_post<C: CalendarStore + ?Sized, S: SubscriptionStore + ?Sized>(
path: Path<(String, String)>,
body: String,
user: User,
store: Data<C>,
subscription_store: Data<S>,
root_span: RootSpan,
req: HttpRequest,
) -> Result<HttpResponse, Error> {
let (principal, cal_id) = path.into_inner();
if principal != user.id {
return Err(Error::Unauthorized);
}
let calendar = store.get_calendar(&principal, &cal_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: calendar.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())
}
#[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())
}
)
}
}

View File

@@ -1,4 +1,5 @@
use super::methods::mkcalendar::route_mkcalendar;
use super::methods::post::route_post;
use super::methods::report::route_report_calendar;
use super::prop::{
SupportedCalendarComponentSet, SupportedCalendarData, SupportedReportSet, Transports,
@@ -15,8 +16,9 @@ use rustical_dav::privileges::UserPrivilegeSet;
use rustical_dav::resource::{Resource, ResourceService};
use rustical_dav::xml::{HrefElement, Resourcetype, ResourcetypeInner};
use rustical_store::auth::User;
use rustical_store::{Calendar, CalendarStore};
use rustical_store::{Calendar, CalendarStore, SubscriptionStore};
use rustical_xml::{XmlDeserialize, XmlSerialize};
use std::marker::PhantomData;
use std::str::FromStr;
use std::sync::Arc;
use strum::{EnumDiscriminants, EnumString, IntoStaticStr, VariantNames};
@@ -254,18 +256,24 @@ impl Resource for CalendarResource {
}
}
pub struct CalendarResourceService<C: CalendarStore + ?Sized> {
pub struct CalendarResourceService<C: CalendarStore + ?Sized, S: SubscriptionStore + ?Sized> {
cal_store: Arc<C>,
__phantom_sub: PhantomData<S>,
}
impl<C: CalendarStore + ?Sized> CalendarResourceService<C> {
impl<C: CalendarStore + ?Sized, S: SubscriptionStore + ?Sized> CalendarResourceService<C, S> {
pub fn new(cal_store: Arc<C>) -> Self {
Self { cal_store }
Self {
cal_store,
__phantom_sub: PhantomData,
}
}
}
#[async_trait(?Send)]
impl<C: CalendarStore + ?Sized> ResourceService for CalendarResourceService<C> {
impl<C: CalendarStore + ?Sized, S: SubscriptionStore + ?Sized> ResourceService
for CalendarResourceService<C, S>
{
type MemberType = CalendarObjectResource;
type PathComponents = (String, String); // principal, calendar_id
type Resource = CalendarResource;
@@ -332,5 +340,6 @@ impl<C: CalendarStore + ?Sized> ResourceService for CalendarResourceService<C> {
res.route(report_method.to(route_report_calendar::<C>))
.route(mkcalendar_method.to(route_mkcalendar::<C>))
.post(route_post::<C, S>)
}
}

View File

@@ -11,14 +11,16 @@ use principal::{PrincipalResource, PrincipalResourceService};
use rustical_dav::resource::{NamedRoute, ResourceService, ResourceServiceRoute};
use rustical_dav::resources::RootResourceService;
use rustical_store::auth::{AuthenticationMiddleware, AuthenticationProvider};
use rustical_store::{AddressbookStore, CalendarStore, ContactBirthdayStore};
use rustical_store::{AddressbookStore, CalendarStore, ContactBirthdayStore, SubscriptionStore};
use std::sync::Arc;
use subscription::subscription_resource;
pub mod calendar;
pub mod calendar_object;
pub mod calendar_set;
pub mod error;
pub mod principal;
mod subscription;
pub use error::Error;
@@ -30,11 +32,13 @@ pub fn configure_dav<
AP: AuthenticationProvider,
AS: AddressbookStore + ?Sized,
C: CalendarStore + ?Sized,
S: SubscriptionStore + ?Sized,
>(
cfg: &mut web::ServiceConfig,
auth_provider: Arc<AP>,
store: Arc<C>,
addr_store: Arc<AS>,
subscription_store: Arc<S>,
) {
let birthday_store = Arc::new(ContactBirthdayStore::new(addr_store));
cfg.service(
@@ -62,6 +66,7 @@ pub fn configure_dav<
)
.app_data(Data::from(store.clone()))
.app_data(Data::from(birthday_store.clone()))
.app_data(Data::from(subscription_store))
.service(RootResourceService::<PrincipalResource>::default().actix_resource())
.service(
web::scope("/user").service(
@@ -74,7 +79,7 @@ pub fn configure_dav<
.service(
web::scope("/{calendar}")
.service(
ResourceServiceRoute(CalendarResourceService::new(store.clone()))
ResourceServiceRoute(CalendarResourceService::<_, S>::new(store.clone()))
)
.service(web::scope("/{object}").service(CalendarObjectResourceService::new(store.clone()).actix_resource()
))
@@ -85,13 +90,13 @@ pub fn configure_dav<
.service(
web::scope("/{calendar}")
.service(
ResourceServiceRoute(CalendarResourceService::new(birthday_store.clone()))
ResourceServiceRoute(CalendarResourceService::<_, S>::new(birthday_store.clone()))
)
.service(web::scope("/{object}").service(CalendarObjectResourceService::new(birthday_store.clone()).actix_resource()
))
)
)
),
),
).service(subscription_resource::<S>()),
);
}

View File

@@ -0,0 +1,20 @@
use actix_web::{
web::{self, Data, Path},
HttpResponse,
};
use rustical_store::SubscriptionStore;
async fn handle_delete<S: SubscriptionStore + ?Sized>(
store: Data<S>,
path: Path<String>,
) -> Result<HttpResponse, rustical_store::Error> {
let id = path.into_inner();
store.delete_subscription(&id).await?;
Ok(HttpResponse::NoContent().body("Unregistered"))
}
pub fn subscription_resource<S: SubscriptionStore + ?Sized>() -> actix_web::Resource {
web::resource("/subscription/{id}")
.name("subscription")
.delete(handle_delete::<S>)
}