DAV Push: Configurable list of allowed push targets

This commit is contained in:
Lennart
2025-01-15 18:05:02 +01:00
parent 4a78704cfa
commit 83d875133f
4 changed files with 85 additions and 29 deletions

View File

@@ -1,10 +1,11 @@
use crate::xml::multistatus::PropstatElement; use crate::xml::multistatus::PropstatElement;
use actix_web::http::StatusCode; use actix_web::http::StatusCode;
use reqwest::Url;
use rustical_store::{CollectionOperation, CollectionOperationType, SubscriptionStore}; use rustical_store::{CollectionOperation, CollectionOperationType, SubscriptionStore};
use rustical_xml::{XmlRootTag, XmlSerialize, XmlSerializeRoot}; use rustical_xml::{XmlRootTag, XmlSerialize, XmlSerializeRoot};
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::mpsc::Receiver; use tokio::sync::mpsc::Receiver;
use tracing::{error, info}; use tracing::{error, info, warn};
#[derive(XmlSerialize, Debug)] #[derive(XmlSerialize, Debug)]
struct PushMessageProp { struct PushMessageProp {
@@ -23,15 +24,26 @@ struct PushMessage {
} }
pub async fn push_notifier( pub async fn push_notifier(
allowed_push_servers: Option<Vec<String>>,
mut recv: Receiver<CollectionOperation>, mut recv: Receiver<CollectionOperation>,
sub_store: Arc<impl SubscriptionStore>, sub_store: Arc<impl SubscriptionStore>,
) { ) {
let client = reqwest::Client::new();
while let Some(message) = recv.recv().await { while let Some(message) = recv.recv().await {
if let Ok(subscribers) = sub_store.get_subscriptions(&message.topic).await { let subscribers = match sub_store.get_subscriptions(&message.topic).await {
Ok(subs) => subs,
Err(err) => {
error!("{err}");
continue;
}
};
let status = match message.r#type { let status = match message.r#type {
CollectionOperationType::Object => StatusCode::OK, CollectionOperationType::Object => StatusCode::OK,
CollectionOperationType::Delete => StatusCode::NOT_FOUND, CollectionOperationType::Delete => StatusCode::NOT_FOUND,
}; };
let push_message = PushMessage { let push_message = PushMessage {
propstat: PropstatElement { propstat: PropstatElement {
prop: PushMessageProp { prop: PushMessageProp {
@@ -41,6 +53,7 @@ pub async fn push_notifier(
status, status,
}, },
}; };
let mut output: Vec<_> = b"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n".into(); let mut output: Vec<_> = b"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n".into();
let mut writer = quick_xml::Writer::new_with_indent(&mut output, b' ', 4); let mut writer = quick_xml::Writer::new_with_indent(&mut output, b' ', 4);
if let Err(err) = push_message.serialize_root(&mut writer) { if let Err(err) = push_message.serialize_root(&mut writer) {
@@ -49,19 +62,33 @@ pub async fn push_notifier(
} }
let payload = String::from_utf8(output).unwrap(); let payload = String::from_utf8(output).unwrap();
for subscriber in subscribers { for subscriber in subscribers {
info!( let push_resource = subscriber.push_resource;
"Sending a push message to {}: {}", let allowed = if let Some(allowed_push_servers) = &allowed_push_servers {
subscriber.push_resource, payload if let Ok(resource_url) = reqwest::Url::parse(&push_resource) {
); let origin = resource_url.origin().ascii_serialization();
let client = reqwest::Client::new(); 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 if let Err(err) = client
.post(subscriber.push_resource) .post(push_resource)
.body(payload.to_owned()) .body(payload.to_owned())
.send() .send()
.await .await
{ {
error!("{err}"); error!("{err}");
} }
} else {
warn!("Not sending a push notification to {} since it's not allowed in dav_push::allowed_push_servers", push_resource);
} }
} }
} }

View File

@@ -7,7 +7,8 @@ use rustical_frontend::FrontendConfig;
use rustical_store::auth::{static_user_store::UserEntry, StaticUserStoreConfig, User}; use rustical_store::auth::{static_user_store::UserEntry, StaticUserStoreConfig, User};
use crate::config::{ use crate::config::{
AuthConfig, Config, DataStoreConfig, HttpConfig, SqliteDataStoreConfig, TracingConfig, AuthConfig, Config, DataStoreConfig, DavPushConfig, HttpConfig, SqliteDataStoreConfig,
TracingConfig,
}; };
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
@@ -46,6 +47,7 @@ pub fn cmd_gen_config(_args: GenConfigArgs) -> anyhow::Result<()> {
frontend: FrontendConfig { frontend: FrontendConfig {
secret_key: generate_frontend_secret(), secret_key: generate_frontend_secret(),
}, },
dav_push: DavPushConfig::default(),
}; };
let generated_config = toml::to_string(&config)?; let generated_config = toml::to_string(&config)?;
println!("{generated_config}"); println!("{generated_config}");

View File

@@ -44,6 +44,25 @@ pub struct TracingConfig {
pub opentelemetry: bool, 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<Vec<String>>,
}
impl Default for DavPushConfig {
fn default() -> Self {
Self {
enable: true,
allowed_push_servers: None,
}
}
}
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
pub struct Config { pub struct Config {
@@ -54,4 +73,6 @@ pub struct Config {
pub frontend: FrontendConfig, pub frontend: FrontendConfig,
#[serde(default)] #[serde(default)]
pub tracing: TracingConfig, pub tracing: TracingConfig,
#[serde(default)]
pub dav_push: DavPushConfig,
} }

View File

@@ -82,7 +82,13 @@ async fn main() -> Result<()> {
let (addr_store, cal_store, subscription_store, update_recv) = let (addr_store, cal_store, subscription_store, update_recv) =
get_data_stores(!args.no_migrations, &config.data_store).await?; 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 { let user_store = Arc::new(match config.auth {
config::AuthConfig::Static(config) => StaticUserStore::new(config), config::AuthConfig::Static(config) => StaticUserStore::new(config),