mirror of
https://github.com/lennart-k/rustical.git
synced 2025-12-13 21:42:34 +00:00
Implement data model changes to support new WebDAV Push spec
This commit is contained in:
@@ -58,6 +58,17 @@ pub async fn route_post<C: CalendarStore, S: SubscriptionStore>(
|
||||
.to_owned(),
|
||||
topic: calendar_resource.cal.push_topic,
|
||||
expiration: expires.naive_local(),
|
||||
public_key: request
|
||||
.subscription
|
||||
.web_push_subscription
|
||||
.subscription_public_key
|
||||
.key,
|
||||
public_key_type: request
|
||||
.subscription
|
||||
.web_push_subscription
|
||||
.subscription_public_key
|
||||
.ty,
|
||||
auth_secret: request.subscription.web_push_subscription.auth_secret,
|
||||
};
|
||||
subscription_store.upsert_subscription(subscription).await?;
|
||||
|
||||
|
||||
@@ -45,6 +45,17 @@ pub async fn route_post<A: AddressbookStore, S: SubscriptionStore>(
|
||||
.to_owned(),
|
||||
topic: addressbook.push_topic,
|
||||
expiration: expires.naive_local(),
|
||||
public_key: request
|
||||
.subscription
|
||||
.web_push_subscription
|
||||
.subscription_public_key
|
||||
.key,
|
||||
public_key_type: request
|
||||
.subscription
|
||||
.web_push_subscription
|
||||
.subscription_public_key
|
||||
.ty,
|
||||
auth_secret: request.subscription.web_push_subscription.auth_secret,
|
||||
};
|
||||
subscription_store.upsert_subscription(subscription).await?;
|
||||
|
||||
|
||||
@@ -23,3 +23,4 @@ reqwest.workspace = true
|
||||
tokio.workspace = true
|
||||
rustical_dav.workspace = true
|
||||
rustical_store.workspace = true
|
||||
web-push = { version = "0.11", default-features = false }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{ContentUpdate, PropertyUpdate, SupportedTriggers, Transports, Trigger};
|
||||
use rustical_dav::header::Depth;
|
||||
use rustical_xml::{EnumUnitVariants, EnumVariants, Unparsed, XmlDeserialize, XmlSerialize};
|
||||
use rustical_xml::{EnumUnitVariants, EnumVariants, XmlDeserialize, XmlSerialize};
|
||||
|
||||
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumUnitVariants, EnumVariants)]
|
||||
#[xml(unit_variants_ident = "DavPushExtensionPropName")]
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
use actix_web::http::StatusCode;
|
||||
use reqwest::{
|
||||
Method, Request,
|
||||
header::{self, HeaderName, HeaderValue},
|
||||
};
|
||||
use rustical_dav::xml::multistatus::PropstatElement;
|
||||
use rustical_store::{CollectionOperation, CollectionOperationType, SubscriptionStore};
|
||||
use rustical_xml::{XmlRootTag, XmlSerialize, XmlSerializeRoot};
|
||||
use std::sync::Arc;
|
||||
use std::{str::FromStr, sync::Arc};
|
||||
use tokio::sync::mpsc::Receiver;
|
||||
use tracing::{error, info, warn};
|
||||
use web_push::{SubscriptionInfo, WebPushMessage, WebPushMessageBuilder};
|
||||
|
||||
#[derive(XmlSerialize, Debug)]
|
||||
struct PushMessageProp {
|
||||
@@ -25,6 +30,40 @@ struct PushMessage {
|
||||
propstat: PropstatElement<PushMessageProp>,
|
||||
}
|
||||
|
||||
pub fn build_request(message: WebPushMessage) -> Request {
|
||||
// A little janky :)
|
||||
let url = reqwest::Url::from_str(&message.endpoint.to_string()).unwrap();
|
||||
let mut builder = Request::new(Method::POST, url);
|
||||
|
||||
if let Some(topic) = message.topic {
|
||||
builder
|
||||
.headers_mut()
|
||||
.insert("Topic", HeaderValue::from_str(topic.as_str()).unwrap());
|
||||
}
|
||||
|
||||
if let Some(payload) = message.payload {
|
||||
builder.headers_mut().insert(
|
||||
header::CONTENT_ENCODING,
|
||||
HeaderValue::from_static(payload.content_encoding.to_str()),
|
||||
);
|
||||
builder.headers_mut().insert(
|
||||
header::CONTENT_TYPE,
|
||||
HeaderValue::from_static("application/octet-stream"),
|
||||
);
|
||||
|
||||
for (k, v) in payload.crypto_headers.into_iter() {
|
||||
let v: &str = v.as_ref();
|
||||
builder.headers_mut().insert(
|
||||
HeaderName::from_static(k),
|
||||
HeaderValue::from_str(&v).unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
*builder.body_mut() = Some(reqwest::Body::from(payload.content));
|
||||
}
|
||||
builder
|
||||
}
|
||||
|
||||
pub async fn push_notifier(
|
||||
allowed_push_servers: Option<Vec<String>>,
|
||||
mut recv: Receiver<CollectionOperation>,
|
||||
@@ -65,6 +104,19 @@ pub async fn push_notifier(
|
||||
let payload = String::from_utf8(output).unwrap();
|
||||
for subscriber in subscribers {
|
||||
let push_resource = subscriber.push_resource;
|
||||
|
||||
let sub_info = SubscriptionInfo {
|
||||
endpoint: push_resource.to_owned(),
|
||||
keys: web_push::SubscriptionKeys {
|
||||
p256dh: subscriber.public_key,
|
||||
auth: subscriber.auth_secret,
|
||||
},
|
||||
};
|
||||
let mut builder = WebPushMessageBuilder::new(&sub_info);
|
||||
builder.set_payload(web_push::ContentEncoding::Aes128Gcm, payload.as_bytes());
|
||||
let push_message = builder.build().unwrap();
|
||||
let request = build_request(push_message);
|
||||
|
||||
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();
|
||||
@@ -81,12 +133,7 @@ pub async fn push_notifier(
|
||||
|
||||
if allowed {
|
||||
info!("Sending a push message to {}: {}", push_resource, payload);
|
||||
if let Err(err) = client
|
||||
.post(push_resource)
|
||||
.body(payload.to_owned())
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
if let Err(err) = client.execute(request).await {
|
||||
error!("{err}");
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -18,9 +18,9 @@ pub struct WebPushSubscription {
|
||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
|
||||
pub struct SubscriptionPublicKey {
|
||||
#[xml(ty = "attr", rename = b"type")]
|
||||
ty: String,
|
||||
pub ty: String,
|
||||
#[xml(ty = "text")]
|
||||
key: String,
|
||||
pub key: String,
|
||||
}
|
||||
|
||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
|
||||
|
||||
@@ -7,6 +7,9 @@ pub struct Subscription {
|
||||
pub topic: String,
|
||||
pub expiration: NaiveDateTime,
|
||||
pub push_resource: String,
|
||||
pub public_key: String,
|
||||
pub public_key_type: String,
|
||||
pub auth_secret: String,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
ALTER TABLE davpush_subscriptions
|
||||
DROP public_key,
|
||||
DROP public_key_type,
|
||||
DROP auth_secret;
|
||||
@@ -0,0 +1,7 @@
|
||||
-- Old subscriptions are useless anyway
|
||||
DELETE FROM davpush_subscriptions;
|
||||
|
||||
-- Now the new columns can also be set NOT NULL
|
||||
ALTER TABLE davpush_subscriptions ADD public_key TEXT NOT NULL;
|
||||
ALTER TABLE davpush_subscriptions ADD public_key_type TEXT NOT NULL;
|
||||
ALTER TABLE davpush_subscriptions ADD auth_secret TEXT NOT NULL;
|
||||
@@ -7,7 +7,7 @@ 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
|
||||
r#"SELECT id, topic, expiration, push_resource, public_key, public_key_type, auth_secret
|
||||
FROM davpush_subscriptions
|
||||
WHERE (topic) = (?)"#,
|
||||
topic
|
||||
@@ -20,7 +20,7 @@ impl SubscriptionStore for SqliteStore {
|
||||
async fn get_subscription(&self, id: &str) -> Result<Subscription, Error> {
|
||||
Ok(sqlx::query_as!(
|
||||
Subscription,
|
||||
r#"SELECT id, topic, expiration, push_resource
|
||||
r#"SELECT id, topic, expiration, push_resource, public_key, public_key_type, auth_secret
|
||||
FROM davpush_subscriptions
|
||||
WHERE (id) = (?)"#,
|
||||
id
|
||||
@@ -32,11 +32,14 @@ impl SubscriptionStore for SqliteStore {
|
||||
|
||||
async fn upsert_subscription(&self, sub: Subscription) -> Result<bool, Error> {
|
||||
sqlx::query!(
|
||||
r#"INSERT OR REPLACE INTO davpush_subscriptions (id, topic, expiration, push_resource) VALUES (?, ?, ?, ?)"#,
|
||||
r#"INSERT OR REPLACE INTO davpush_subscriptions (id, topic, expiration, push_resource, public_key, public_key_type, auth_secret) VALUES (?, ?, ?, ?, ?, ?, ?)"#,
|
||||
sub.id,
|
||||
sub.topic,
|
||||
sub.expiration,
|
||||
sub.push_resource
|
||||
sub.push_resource,
|
||||
sub.public_key,
|
||||
sub.public_key_type,
|
||||
sub.auth_secret
|
||||
).execute(&self.db).await.map_err(crate::Error::from)?;
|
||||
// TODO: Correctly return whether a subscription already existed
|
||||
Ok(false)
|
||||
|
||||
Reference in New Issue
Block a user