Skip to content

Commit

Permalink
Dav Push: Logic to register subscriptions
Browse files Browse the repository at this point in the history
  • Loading branch information
lennart-k committed Jan 12, 2025
1 parent 185eb8b commit 5b418ec
Show file tree
Hide file tree
Showing 12 changed files with 261 additions and 15 deletions.
1 change: 1 addition & 0 deletions crates/caldav/src/calendar/methods/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
pub mod mkcalendar;
pub mod post;
pub mod report;
115 changes: 115 additions & 0 deletions crates/caldav/src/calendar/methods/post.rs
Original file line number Diff line number Diff line change
@@ -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())
}
)
}
}
19 changes: 14 additions & 5 deletions crates/caldav/src/calendar/resource.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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};
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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>)
}
}
13 changes: 9 additions & 4 deletions crates/caldav/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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(
Expand Down Expand Up @@ -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(
Expand All @@ -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()
))
Expand All @@ -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>()),
);
}
20 changes: 20 additions & 0 deletions crates/caldav/src/subscription.rs
Original file line number Diff line number Diff line change
@@ -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>)
}
2 changes: 2 additions & 0 deletions crates/store/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ pub use error::Error;
pub mod auth;
pub mod calendar;
mod contact_birthday_store;
mod subscription_store;
pub mod synctoken;

pub use addressbook_store::AddressbookStore;
pub use calendar_store::CalendarStore;
pub use contact_birthday_store::ContactBirthdayStore;
pub use subscription_store::*;

pub use addressbook::{AddressObject, Addressbook};
pub use calendar::{Calendar, CalendarObject};
19 changes: 19 additions & 0 deletions crates/store/src/subscription_store.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
use crate::Error;
use async_trait::async_trait;
use chrono::NaiveDateTime;

pub struct Subscription {
pub id: String,
pub topic: String,
pub expiration: NaiveDateTime,
pub push_resource: String,
}

#[async_trait(?Send)]
pub trait SubscriptionStore: Send + Sync + 'static {
async fn get_subscriptions(&self, topic: &str) -> Result<Vec<Subscription>, Error>;
async fn get_subscription(&self, id: &str) -> Result<Subscription, Error>;
/// Returns whether a subscription under the id already existed
async fn upsert_subscription(&self, sub: Subscription) -> Result<bool, Error>;
async fn delete_subscription(&self, id: &str) -> Result<(), Error>;
}
7 changes: 7 additions & 0 deletions crates/store_sqlite/migrations/3_subscriptions.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
CREATE TABLE subscriptions (
id TEXT NOT NULL,
topic TEXT NOT NULL,
expiration DATETIME NOT NULL,
push_resource TEXT NOT NULL,
PRIMARY KEY (id)
);
1 change: 1 addition & 0 deletions crates/store_sqlite/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use sqlx::{sqlite::SqliteConnectOptions, Pool, Sqlite, SqlitePool};
pub mod addressbook_store;
pub mod calendar_store;
pub mod error;
pub mod subscription_store;

pub use error::Error;

Expand Down
51 changes: 51 additions & 0 deletions crates/store_sqlite/src/subscription_store.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
use crate::SqliteStore;
use async_trait::async_trait;
use rustical_store::{Error, Subscription, SubscriptionStore};

#[async_trait(?Send)]
impl SubscriptionStore for SqliteStore {
async fn get_subscriptions(&self, topic: &str) -> Result<Vec<Subscription>, Error> {
Ok(sqlx::query_as!(
Subscription,
r#"SELECT id, topic, expiration, push_resource
FROM subscriptions
WHERE (topic) = (?)"#,
topic
)
.fetch_all(&self.db)
.await
.map_err(crate::Error::from)?)
}

async fn get_subscription(&self, id: &str) -> Result<Subscription, Error> {
Ok(sqlx::query_as!(
Subscription,
r#"SELECT id, topic, expiration, push_resource
FROM subscriptions
WHERE (id) = (?)"#,
id
)
.fetch_one(&self.db)
.await
.map_err(crate::Error::from)?)
}

async fn upsert_subscription(&self, sub: Subscription) -> Result<bool, Error> {
sqlx::query!(
r#"INSERT OR REPLACE INTO subscriptions (id, topic, expiration, push_resource) VALUES (?, ?, ?, ?)"#,
sub.id,
sub.topic,
sub.expiration,
sub.push_resource
).execute(&self.db).await.map_err(crate::Error::from)?;
// TODO: Correctly return whether a subscription already existed
Ok(false)
}
async fn delete_subscription(&self, id: &str) -> Result<(), Error> {
sqlx::query!(r#"DELETE FROM subscriptions WHERE id = ? "#, id)
.execute(&self.db)
.await
.map_err(crate::Error::from)?;
Ok(())
}
}
11 changes: 9 additions & 2 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,19 @@ use actix_web::middleware::NormalizePath;
use actix_web::{web, App};
use rustical_frontend::{configure_frontend, FrontendConfig};
use rustical_store::auth::AuthenticationProvider;
use rustical_store::{AddressbookStore, CalendarStore};
use rustical_store::{AddressbookStore, CalendarStore, SubscriptionStore};
use rustical_store_sqlite::subscription_store;
use std::sync::Arc;
use tracing_actix_web::TracingLogger;

pub fn make_app<AS: AddressbookStore + ?Sized, CS: CalendarStore + ?Sized>(
pub fn make_app<
AS: AddressbookStore + ?Sized,
CS: CalendarStore + ?Sized,
S: SubscriptionStore + ?Sized,
>(
addr_store: Arc<AS>,
cal_store: Arc<CS>,
subscription_store: Arc<S>,
auth_provider: Arc<impl AuthenticationProvider>,
frontend_config: FrontendConfig,
) -> App<
Expand All @@ -32,6 +38,7 @@ pub fn make_app<AS: AddressbookStore + ?Sized, CS: CalendarStore + ?Sized>(
auth_provider.clone(),
cal_store.clone(),
addr_store.clone(),
subscription_store,
)
}))
.service(web::scope("/carddav").configure(|cfg| {
Expand Down
Loading

0 comments on commit 5b418ec

Please sign in to comment.