mirror of
https://github.com/lennart-k/rustical.git
synced 2025-12-13 18:12:27 +00:00
Dav Push: Logic to register subscriptions
This commit is contained in:
@@ -1,2 +1,3 @@
|
||||
pub mod mkcalendar;
|
||||
pub mod post;
|
||||
pub mod report;
|
||||
|
||||
115
crates/caldav/src/calendar/methods/post.rs
Normal file
115
crates/caldav/src/calendar/methods/post.rs
Normal 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())
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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>)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>()),
|
||||
);
|
||||
}
|
||||
|
||||
20
crates/caldav/src/subscription.rs
Normal file
20
crates/caldav/src/subscription.rs
Normal 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>)
|
||||
}
|
||||
@@ -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
crates/store/src/subscription_store.rs
Normal file
19
crates/store/src/subscription_store.rs
Normal file
@@ -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
crates/store_sqlite/migrations/3_subscriptions.sql
Normal file
7
crates/store_sqlite/migrations/3_subscriptions.sql
Normal file
@@ -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)
|
||||
);
|
||||
@@ -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;
|
||||
|
||||
|
||||
51
crates/store_sqlite/src/subscription_store.rs
Normal file
51
crates/store_sqlite/src/subscription_store.rs
Normal file
@@ -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
src/app.rs
11
src/app.rs
@@ -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<
|
||||
@@ -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| {
|
||||
|
||||
17
src/main.rs
17
src/main.rs
@@ -7,7 +7,7 @@ use clap::{Parser, Subcommand};
|
||||
use commands::{cmd_gen_config, cmd_pwhash};
|
||||
use config::{DataStoreConfig, SqliteDataStoreConfig};
|
||||
use rustical_store::auth::StaticUserStore;
|
||||
use rustical_store::{AddressbookStore, CalendarStore};
|
||||
use rustical_store::{AddressbookStore, CalendarStore, SubscriptionStore};
|
||||
use rustical_store_sqlite::{create_db_pool, SqliteStore};
|
||||
use setup_tracing::setup_tracing;
|
||||
use std::fs;
|
||||
@@ -39,12 +39,20 @@ enum Command {
|
||||
async fn get_data_stores(
|
||||
migrate: bool,
|
||||
config: &DataStoreConfig,
|
||||
) -> Result<(Arc<dyn AddressbookStore>, Arc<dyn CalendarStore>)> {
|
||||
) -> Result<(
|
||||
Arc<dyn AddressbookStore>,
|
||||
Arc<dyn CalendarStore>,
|
||||
Arc<dyn SubscriptionStore>,
|
||||
)> {
|
||||
Ok(match &config {
|
||||
DataStoreConfig::Sqlite(SqliteDataStoreConfig { db_url }) => {
|
||||
let db = create_db_pool(db_url, migrate).await?;
|
||||
let sqlite_store = Arc::new(SqliteStore::new(db));
|
||||
(sqlite_store.clone(), sqlite_store.clone())
|
||||
(
|
||||
sqlite_store.clone(),
|
||||
sqlite_store.clone(),
|
||||
sqlite_store.clone(),
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -61,7 +69,7 @@ async fn main() -> Result<()> {
|
||||
|
||||
setup_tracing(&config.tracing);
|
||||
|
||||
let (addr_store, cal_store) =
|
||||
let (addr_store, cal_store, subscription_store) =
|
||||
get_data_stores(!args.no_migrations, &config.data_store).await?;
|
||||
|
||||
let user_store = Arc::new(match config.auth {
|
||||
@@ -72,6 +80,7 @@ async fn main() -> Result<()> {
|
||||
make_app(
|
||||
addr_store.clone(),
|
||||
cal_store.clone(),
|
||||
subscription_store.clone(),
|
||||
user_store.clone(),
|
||||
config.frontend.clone(),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user