From 83d875133f1baa5c084c9a867315f2677b083432 Mon Sep 17 00:00:00 2001 From: Lennart <18233294+lennart-k@users.noreply.github.com> Date: Wed, 15 Jan 2025 18:05:02 +0100 Subject: [PATCH] DAV Push: Configurable list of allowed push targets --- crates/dav/src/push/push_notifier.rs | 81 ++++++++++++++++++---------- src/commands/mod.rs | 4 +- src/config.rs | 21 ++++++++ src/main.rs | 8 ++- 4 files changed, 85 insertions(+), 29 deletions(-) diff --git a/crates/dav/src/push/push_notifier.rs b/crates/dav/src/push/push_notifier.rs index 8895c71..f8f5f2d 100644 --- a/crates/dav/src/push/push_notifier.rs +++ b/crates/dav/src/push/push_notifier.rs @@ -1,10 +1,11 @@ use crate::xml::multistatus::PropstatElement; use actix_web::http::StatusCode; +use reqwest::Url; use rustical_store::{CollectionOperation, CollectionOperationType, SubscriptionStore}; use rustical_xml::{XmlRootTag, XmlSerialize, XmlSerializeRoot}; use std::sync::Arc; use tokio::sync::mpsc::Receiver; -use tracing::{error, info}; +use tracing::{error, info, warn}; #[derive(XmlSerialize, Debug)] struct PushMessageProp { @@ -23,45 +24,71 @@ struct PushMessage { } pub async fn push_notifier( + allowed_push_servers: Option>, mut recv: Receiver, sub_store: Arc, ) { + let client = reqwest::Client::new(); + while let Some(message) = recv.recv().await { - if let Ok(subscribers) = sub_store.get_subscriptions(&message.topic).await { - let status = match message.r#type { - CollectionOperationType::Object => StatusCode::OK, - CollectionOperationType::Delete => StatusCode::NOT_FOUND, - }; - let push_message = PushMessage { - propstat: PropstatElement { - prop: PushMessageProp { - topic: message.topic, - sync_token: message.sync_token, - }, - status, - }, - }; - let mut output: Vec<_> = b"\n".into(); - let mut writer = quick_xml::Writer::new_with_indent(&mut output, b' ', 4); - if let Err(err) = push_message.serialize_root(&mut writer) { - error!("Could not serialize push message: {}", err); + let subscribers = match sub_store.get_subscriptions(&message.topic).await { + Ok(subs) => subs, + Err(err) => { + error!("{err}"); continue; } - let payload = String::from_utf8(output).unwrap(); - for subscriber in subscribers { - info!( - "Sending a push message to {}: {}", - subscriber.push_resource, payload - ); - let client = reqwest::Client::new(); + }; + + let status = match message.r#type { + CollectionOperationType::Object => StatusCode::OK, + CollectionOperationType::Delete => StatusCode::NOT_FOUND, + }; + + let push_message = PushMessage { + propstat: PropstatElement { + prop: PushMessageProp { + topic: message.topic, + sync_token: message.sync_token, + }, + status, + }, + }; + + let mut output: Vec<_> = b"\n".into(); + let mut writer = quick_xml::Writer::new_with_indent(&mut output, b' ', 4); + if let Err(err) = push_message.serialize_root(&mut writer) { + error!("Could not serialize push message: {}", err); + continue; + } + let payload = String::from_utf8(output).unwrap(); + for subscriber in subscribers { + let push_resource = subscriber.push_resource; + let allowed = if let Some(allowed_push_servers) = &allowed_push_servers { + if let Ok(resource_url) = reqwest::Url::parse(&push_resource) { + let origin = resource_url.origin().ascii_serialization(); + allowed_push_servers + .iter() + .any(|allowed_push_server| allowed_push_server == &origin) + } else { + warn!("Invalid push url: {push_resource}"); + false + } + } else { + true + }; + + if allowed { + info!("Sending a push message to {}: {}", push_resource, payload); if let Err(err) = client - .post(subscriber.push_resource) + .post(push_resource) .body(payload.to_owned()) .send() .await { error!("{err}"); } + } else { + warn!("Not sending a push notification to {} since it's not allowed in dav_push::allowed_push_servers", push_resource); } } } diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 8d6aac3..a263948 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -7,7 +7,8 @@ use rustical_frontend::FrontendConfig; use rustical_store::auth::{static_user_store::UserEntry, StaticUserStoreConfig, User}; use crate::config::{ - AuthConfig, Config, DataStoreConfig, HttpConfig, SqliteDataStoreConfig, TracingConfig, + AuthConfig, Config, DataStoreConfig, DavPushConfig, HttpConfig, SqliteDataStoreConfig, + TracingConfig, }; #[derive(Debug, Parser)] @@ -46,6 +47,7 @@ pub fn cmd_gen_config(_args: GenConfigArgs) -> anyhow::Result<()> { frontend: FrontendConfig { secret_key: generate_frontend_secret(), }, + dav_push: DavPushConfig::default(), }; let generated_config = toml::to_string(&config)?; println!("{generated_config}"); diff --git a/src/config.rs b/src/config.rs index 3edd299..003c6c3 100644 --- a/src/config.rs +++ b/src/config.rs @@ -44,6 +44,25 @@ pub struct TracingConfig { pub opentelemetry: bool, } +#[derive(Debug, Deserialize, Serialize)] +#[serde(deny_unknown_fields, default)] +pub struct DavPushConfig { + pub enable: bool, + #[serde(default)] + // Allowed Push servers, accepts any by default + // Specify as URL origins + pub allowed_push_servers: Option>, +} + +impl Default for DavPushConfig { + fn default() -> Self { + Self { + enable: true, + allowed_push_servers: None, + } + } +} + #[derive(Debug, Deserialize, Serialize)] #[serde(deny_unknown_fields)] pub struct Config { @@ -54,4 +73,6 @@ pub struct Config { pub frontend: FrontendConfig, #[serde(default)] pub tracing: TracingConfig, + #[serde(default)] + pub dav_push: DavPushConfig, } diff --git a/src/main.rs b/src/main.rs index d6e22d7..21547e8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -82,7 +82,13 @@ async fn main() -> Result<()> { let (addr_store, cal_store, subscription_store, update_recv) = get_data_stores(!args.no_migrations, &config.data_store).await?; - tokio::spawn(push_notifier(update_recv, subscription_store.clone())); + if config.dav_push.enable { + tokio::spawn(push_notifier( + config.dav_push.allowed_push_servers, + update_recv, + subscription_store.clone(), + )); + } let user_store = Arc::new(match config.auth { config::AuthConfig::Static(config) => StaticUserStore::new(config),