Compare commits

..

25 Commits

Author SHA1 Message Date
Lennart
786b15f5b9 version 0.3.4 2025-06-22 23:58:49 +02:00
Lennart
f5d097ac55 oidc: Fix for OIDC servers not supporting RFC 9207
see #81
2025-06-22 23:55:57 +02:00
Lennart
668fa86e3c Update version to 0.3.3 2025-06-22 21:46:37 +02:00
Lennart
23d2024644 Update note on production-readiness 2025-06-22 19:43:46 +02:00
Lennart
15aadcf1be Rename User struct to Principal 2025-06-19 20:59:59 +02:00
Lennart
4a3b7d7ce6 Update typescript config 2025-06-19 20:52:17 +02:00
Lennart
1a2f3b8f8a frontend: Move collection creation to dialog 2025-06-18 18:09:19 +02:00
Lennart
9e8c218308 Remove unused p256 dependency 2025-06-18 17:49:00 +02:00
Lennart
f2adce739b Update version to v0.3.2 2025-06-15 17:12:34 +02:00
Lennart
0415664ff3 calendar_store: Fix deleted objects being returned 2025-06-15 16:31:07 +02:00
Lennart
677e0082fa multistatus response: Set No-Cache 2025-06-15 13:16:37 +02:00
Lennart
a387885b0a Remove calendar-proxy-write from caldav principal 2025-06-15 11:44:44 +02:00
Lennart
990b953055 Fix typo on store preventing us from deleting calendar objects 2025-06-15 10:37:51 +02:00
Lennart
36b47a645d Fix missing ece backend, finally managed to statically link openssl 2025-06-14 22:26:01 +02:00
Lennart
aa02d11f58 Increase version number to 0.3.0 2025-06-14 20:33:25 +02:00
Lennart
1c31323512 Remove optional dependencies to remove openssl dependency 2025-06-14 20:32:10 +02:00
Lennart
03ae492483 Implement DAV Push 2025-06-14 20:24:50 +02:00
Lennart
0c48507f0c dav: Fix Destination header percent decoding 2025-06-14 16:49:34 +02:00
Lennart
829d4a4385 dav: MOVE/COPY remove origin from Destination header 2025-06-14 15:46:39 +02:00
Lennart
4fe28c5b0f dav: Make MethodFunction public 2025-06-14 15:24:23 +02:00
Lennart
529f36ad99 dav: Convert is_collection const to function which will make filesystem access easier 2025-06-14 15:21:10 +02:00
Lennart
ca5891314c Forgot to commit Cargo.lock 2025-06-14 14:58:33 +02:00
Lennart
e653c68cae Set log level for 404 2025-06-14 14:57:42 +02:00
Lennart
26941c621b Update version to v0.2.2 2025-06-14 14:44:47 +02:00
Lennart
86ab6ef75e dav: Add interface for copy and move 2025-06-14 14:44:10 +02:00
83 changed files with 3118 additions and 1005 deletions

View File

@@ -1,6 +1,6 @@
{ {
"db_name": "SQLite", "db_name": "SQLite",
"query": "SELECT id, vcf FROM addressobjects WHERE (principal, addressbook_id, id) = (?, ?, ?) AND ((deleted_at IS NULL) or ?)", "query": "SELECT id, vcf FROM addressobjects WHERE (principal, addressbook_id, id) = (?, ?, ?) AND ((deleted_at IS NULL) OR ?)",
"describe": { "describe": {
"columns": [ "columns": [
{ {
@@ -22,5 +22,5 @@
false false
] ]
}, },
"hash": "395e40a7b3333b79bc2ad50a123d99f74bc2712a16257ee2119dd211fdb61f7e" "hash": "246ec675667992c1297c29348d46496a884c59adb8b64b569d36f4ce10f88f47"
} }

View File

@@ -1,6 +1,6 @@
{ {
"db_name": "SQLite", "db_name": "SQLite",
"query": "SELECT id, ics FROM calendarobjects WHERE (principal, cal_id, id) = (?, ?, ?)", "query": "SELECT id, ics FROM calendarobjects WHERE (principal, cal_id, id) = (?, ?, ?) AND ((deleted_at IS NULL) OR ?)",
"describe": { "describe": {
"columns": [ "columns": [
{ {
@@ -15,12 +15,12 @@
} }
], ],
"parameters": { "parameters": {
"Right": 3 "Right": 4
}, },
"nullable": [ "nullable": [
false, false,
false false
] ]
}, },
"hash": "d2f7423e2e8f97607f6664200990dcadb927445880ec6edffba3b5aedf4e199b" "hash": "543838c030550cb09d1af08adfeade8b7ce3575d92fddbc6e9582d141bc9e49d"
} }

121
Cargo.lock generated
View File

@@ -759,6 +759,23 @@ dependencies = [
"spki", "spki",
] ]
[[package]]
name = "ece"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2ea1d2f2cc974957a4e2575d8e5bb494549bab66338d6320c2789abcfff5746"
dependencies = [
"base64 0.21.7",
"byteorder",
"hex",
"hkdf",
"lazy_static",
"once_cell",
"openssl",
"sha2",
"thiserror 1.0.69",
]
[[package]] [[package]]
name = "ed25519" name = "ed25519"
version = "2.2.3" version = "2.2.3"
@@ -903,6 +920,21 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
name = "foreign-types"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [
"foreign-types-shared",
]
[[package]]
name = "foreign-types-shared"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]] [[package]]
name = "form_urlencoded" name = "form_urlencoded"
version = "1.2.1" version = "1.2.1"
@@ -1622,6 +1654,18 @@ version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
[[package]]
name = "matchit-serde"
version = "0.1.0"
source = "git+https://github.com/lennart-k/matchit-serde?rev=f0591d13#f0591d139ea1c88fa4ee397f3fcb4225fad4c6dc"
dependencies = [
"derive_more",
"matchit",
"percent-encoding",
"serde",
"thiserror 2.0.12",
]
[[package]] [[package]]
name = "md-5" name = "md-5"
version = "0.10.6" version = "0.10.6"
@@ -1809,6 +1853,54 @@ dependencies = [
"url", "url",
] ]
[[package]]
name = "openssl"
version = "0.10.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8"
dependencies = [
"bitflags",
"cfg-if",
"foreign-types",
"libc",
"once_cell",
"openssl-macros",
"openssl-sys",
]
[[package]]
name = "openssl-macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "openssl-src"
version = "300.5.0+3.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8ce546f549326b0e6052b649198487d91320875da901e7bd11a06d1ee3f9c2f"
dependencies = [
"cc",
]
[[package]]
name = "openssl-sys"
version = "0.9.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571"
dependencies = [
"cc",
"libc",
"openssl-src",
"pkg-config",
"vcpkg",
]
[[package]] [[package]]
name = "opentelemetry" name = "opentelemetry"
version = "0.30.0" version = "0.30.0"
@@ -2644,7 +2736,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical" name = "rustical"
version = "0.1.0" version = "0.3.4"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"argon2", "argon2",
@@ -2687,7 +2779,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_caldav" name = "rustical_caldav"
version = "0.1.0" version = "0.3.4"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -2722,7 +2814,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_carddav" name = "rustical_carddav"
version = "0.1.0" version = "0.3.4"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -2754,7 +2846,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_dav" name = "rustical_dav"
version = "0.1.0" version = "0.3.4"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -2765,6 +2857,8 @@ dependencies = [
"http", "http",
"itertools 0.14.0", "itertools 0.14.0",
"log", "log",
"matchit",
"matchit-serde",
"quick-xml", "quick-xml",
"rustical_xml", "rustical_xml",
"serde", "serde",
@@ -2777,15 +2871,20 @@ dependencies = [
[[package]] [[package]]
name = "rustical_dav_push" name = "rustical_dav_push"
version = "0.1.0" version = "0.3.4"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum",
"base64 0.22.1",
"derive_more", "derive_more",
"ece",
"futures-util", "futures-util",
"http", "http",
"itertools 0.14.0", "itertools 0.14.0",
"log", "log",
"openssl",
"quick-xml", "quick-xml",
"rand 0.9.1",
"reqwest", "reqwest",
"rustical_dav", "rustical_dav",
"rustical_store", "rustical_store",
@@ -2798,7 +2897,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_frontend" name = "rustical_frontend"
version = "0.1.0" version = "0.3.4"
dependencies = [ dependencies = [
"askama", "askama",
"askama_web", "askama_web",
@@ -2831,7 +2930,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_ical" name = "rustical_ical"
version = "0.1.0" version = "0.3.4"
dependencies = [ dependencies = [
"axum", "axum",
"chrono", "chrono",
@@ -2849,7 +2948,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_oidc" name = "rustical_oidc"
version = "0.1.0" version = "0.3.4"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -2864,7 +2963,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_store" name = "rustical_store"
version = "0.1.0" version = "0.3.4"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@@ -2898,7 +2997,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_store_sqlite" name = "rustical_store_sqlite"
version = "0.1.0" version = "0.3.4"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"chrono", "chrono",
@@ -2918,7 +3017,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_xml" name = "rustical_xml"
version = "0.1.0" version = "0.3.4"
dependencies = [ dependencies = [
"quick-xml", "quick-xml",
"thiserror 2.0.12", "thiserror 2.0.12",

View File

@@ -2,7 +2,7 @@
members = ["crates/*"] members = ["crates/*"]
[workspace.package] [workspace.package]
version = "0.1.0" version = "0.3.4"
edition = "2024" edition = "2024"
description = "A CalDAV server" description = "A CalDAV server"
repository = "https://github.com/lennart-k/rustical" repository = "https://github.com/lennart-k/rustical"
@@ -34,6 +34,7 @@ opentelemetry = [
debug = 0 debug = 0
[workspace.dependencies] [workspace.dependencies]
matchit = "0.8"
uuid = { version = "1.11", features = ["v4", "fast-rng"] } uuid = { version = "1.11", features = ["v4", "fast-rng"] }
async-trait = "0.1" async-trait = "0.1"
axum = "0.8" axum = "0.8"
@@ -133,6 +134,11 @@ reqwest = { version = "0.12", features = [
], default-features = false } ], default-features = false }
openidconnect = "4.0" openidconnect = "4.0"
clap = { version = "4.5", features = ["derive", "env"] } clap = { version = "4.5", features = ["derive", "env"] }
matchit-serde = { git = "https://github.com/lennart-k/matchit-serde", rev = "f0591d13" }
ece = { version = "2.3", default-features = false, features = [
"backend-openssl",
] }
openssl = { version = "0.10", features = ["vendored"] }
[dependencies] [dependencies]
rustical_store = { workspace = true } rustical_store = { workspace = true }

View File

@@ -16,7 +16,7 @@ RUN case $TARGETPLATFORM in \
*) echo "Unsupported platform ${TARGETPLATFORM}"; exit 1;; \ *) echo "Unsupported platform ${TARGETPLATFORM}"; exit 1;; \
esac esac
RUN apk add --no-cache musl-dev llvm19 clang \ RUN apk add --no-cache musl-dev llvm19 clang perl pkgconf make \
&& rustup target add "$(cat /tmp/rust_target)" \ && rustup target add "$(cat /tmp/rust_target)" \
&& cargo install cargo-chef --locked \ && cargo install cargo-chef --locked \
&& rm -rf "$CARGO_HOME/registry" && rm -rf "$CARGO_HOME/registry"

View File

@@ -4,14 +4,15 @@ a CalDAV/CardDAV server
> [!WARNING] > [!WARNING]
RustiCal is **not production-ready!** RustiCal is **not production-ready!**
While I've started migrating to RustiCal and becoming more confident, I've been using RustiCal for the last few weeks and I'm slowly becoming more confident,
please know that bugs and rough edges will still occur. however you'd be one of the first testers so expect bugs and rough edges.
If you still want to play around with it in its current state, absolutely feel free to do so and to open up an issue if something is not working. :) If you still want to play around with it in its current state, absolutely feel free to do so and to open up an issue if something is not working. :)
## Features ## Features
- easy to backup, everything saved in one SQLite database - easy to backup, everything saved in one SQLite database
- ~~[WebDAV Push](https://github.com/bitfireAT/webdav-push/) support, so near-instant synchronisation to DAVx5~~ (currently broken) - also export feature in the frontend
- [WebDAV Push](https://github.com/bitfireAT/webdav-push/) support, so near-instant synchronisation to DAVx5
- lightweight (the container image contains only one binary) - lightweight (the container image contains only one binary)
- adequately fast (I'd love to say blazingly fast™ :fire: but I don't have any benchmarks) - adequately fast (I'd love to say blazingly fast™ :fire: but I don't have any benchmarks)
- deleted calendars are recoverable - deleted calendars are recoverable

View File

@@ -7,5 +7,6 @@ accepted = [
"CDLA-Permissive-2.0", "CDLA-Permissive-2.0",
"Zlib", "Zlib",
"AGPL-3.0", "AGPL-3.0",
"MPL-2.0",
] ]
workarounds = ["ring", "chrono", "rustls"] workarounds = ["ring", "chrono", "rustls"]

View File

@@ -9,7 +9,7 @@ use ical::generator::{Emitter, IcalCalendarBuilder};
use ical::property::Property; use ical::property::Property;
use percent_encoding::{CONTROLS, utf8_percent_encode}; use percent_encoding::{CONTROLS, utf8_percent_encode};
use rustical_ical::{CalendarObjectComponent, EventObject, JournalObject, TodoObject}; use rustical_ical::{CalendarObjectComponent, EventObject, JournalObject, TodoObject};
use rustical_store::{CalendarStore, SubscriptionStore, auth::User}; use rustical_store::{CalendarStore, SubscriptionStore, auth::Principal};
use std::collections::HashMap; use std::collections::HashMap;
use std::str::FromStr; use std::str::FromStr;
use tracing::instrument; use tracing::instrument;
@@ -18,7 +18,7 @@ use tracing::instrument;
pub async fn route_get<C: CalendarStore, S: SubscriptionStore>( pub async fn route_get<C: CalendarStore, S: SubscriptionStore>(
Path((principal, calendar_id)): Path<(String, String)>, Path((principal, calendar_id)): Path<(String, String)>,
State(CalendarResourceService { cal_store, .. }): State<CalendarResourceService<C, S>>, State(CalendarResourceService { cal_store, .. }): State<CalendarResourceService<C, S>>,
user: User, user: Principal,
) -> Result<Response, Error> { ) -> Result<Response, Error> {
if !user.is_principal(&principal) { if !user.is_principal(&principal) {
return Err(crate::Error::Unauthorized); return Err(crate::Error::Unauthorized);

View File

@@ -6,7 +6,7 @@ use axum::response::{IntoResponse, Response};
use http::{Method, StatusCode}; use http::{Method, StatusCode};
use rustical_dav::xml::HrefElement; use rustical_dav::xml::HrefElement;
use rustical_ical::CalendarObjectType; use rustical_ical::CalendarObjectType;
use rustical_store::auth::User; use rustical_store::auth::Principal;
use rustical_store::{Calendar, CalendarStore, SubscriptionStore}; use rustical_store::{Calendar, CalendarStore, SubscriptionStore};
use rustical_xml::{Unparsed, XmlDeserialize, XmlDocument, XmlRootTag}; use rustical_xml::{Unparsed, XmlDeserialize, XmlDocument, XmlRootTag};
use tracing::instrument; use tracing::instrument;
@@ -63,7 +63,7 @@ struct MkcolRequest {
#[instrument(skip(cal_store))] #[instrument(skip(cal_store))]
pub async fn route_mkcalendar<C: CalendarStore, S: SubscriptionStore>( pub async fn route_mkcalendar<C: CalendarStore, S: SubscriptionStore>(
Path((principal, cal_id)): Path<(String, String)>, Path((principal, cal_id)): Path<(String, String)>,
user: User, user: Principal,
State(CalendarResourceService { cal_store, .. }): State<CalendarResourceService<C, S>>, State(CalendarResourceService { cal_store, .. }): State<CalendarResourceService<C, S>>,
method: Method, method: Method,
body: String, body: String,

View File

@@ -1,4 +1,4 @@
pub mod mkcalendar;
// pub mod post;
pub mod get; pub mod get;
pub mod mkcalendar;
pub mod post;
pub mod report; pub mod report;

View File

@@ -1,12 +1,13 @@
use crate::Error; use crate::Error;
use crate::calendar::resource::{CalendarResource, CalendarResourceService}; use crate::calendar::CalendarResourceService;
use crate::calendar::resource::CalendarResource;
use axum::extract::{Path, State}; use axum::extract::{Path, State};
use axum::response::{IntoResponse, Response}; use axum::response::{IntoResponse, Response};
use http::{HeaderMap, StatusCode, header}; use http::{HeaderMap, HeaderValue, StatusCode, header};
use rustical_dav::privileges::UserPrivilege; use rustical_dav::privileges::UserPrivilege;
use rustical_dav::resource::Resource; use rustical_dav::resource::Resource;
use rustical_dav_push::register::PushRegister; use rustical_dav_push::register::PushRegister;
use rustical_store::auth::User; use rustical_store::auth::Principal;
use rustical_store::{CalendarStore, Subscription, SubscriptionStore}; use rustical_store::{CalendarStore, Subscription, SubscriptionStore};
use rustical_xml::XmlDocument; use rustical_xml::XmlDocument;
use tracing::instrument; use tracing::instrument;
@@ -14,7 +15,7 @@ use tracing::instrument;
#[instrument(skip(resource_service))] #[instrument(skip(resource_service))]
pub async fn route_post<C: CalendarStore, S: SubscriptionStore>( pub async fn route_post<C: CalendarStore, S: SubscriptionStore>(
Path((principal, cal_id)): Path<(String, String)>, Path((principal, cal_id)): Path<(String, String)>,
user: User, user: Principal,
State(resource_service): State<CalendarResourceService<C, S>>, State(resource_service): State<CalendarResourceService<C, S>>,
body: String, body: String,
) -> Result<Response, Error> { ) -> Result<Response, Error> {
@@ -73,20 +74,17 @@ pub async fn route_post<C: CalendarStore, S: SubscriptionStore>(
.upsert_subscription(subscription) .upsert_subscription(subscription)
.await?; .await?;
// let location = req // TODO: make nicer
// .resource_map() let location = format!("/push_subscription/{sub_id}");
// .url_for(&req, "subscription", &[sub_id])
// .unwrap();
//
let location = "asd";
Ok(( Ok((
StatusCode::CREATED, StatusCode::CREATED,
HeaderMap::from_iter([(header::LOCATION, location)]), HeaderMap::from_iter([
(header::LOCATION, HeaderValue::from_str(&location).unwrap()),
(
header::EXPIRES,
HeaderValue::from_str(&expires.to_rfc2822()).unwrap(),
),
]),
) )
.into_response()); .into_response())
Ok(HttpResponse::Created()
.append_header((header::LOCATION, location.to_string()))
.append_header((header::EXPIRES, expires.to_rfc2822()))
.finish())
} }

View File

@@ -29,7 +29,7 @@ pub async fn get_objects_calendar_multiget<C: CalendarStore>(
if let Some(filename) = href.strip_prefix(path) { if let Some(filename) = href.strip_prefix(path) {
let filename = filename.trim_start_matches("/"); let filename = filename.trim_start_matches("/");
if let Some(object_id) = filename.strip_suffix(".ics") { if let Some(object_id) = filename.strip_suffix(".ics") {
match store.get_object(principal, cal_id, object_id).await { match store.get_object(principal, cal_id, object_id, false).await {
Ok(object) => result.push(object), Ok(object) => result.push(object),
Err(rustical_store::Error::NotFound) => not_found.push(href.to_owned()), Err(rustical_store::Error::NotFound) => not_found.push(href.to_owned()),
Err(err) => return Err(err.into()), Err(err) => return Err(err.into()),

View File

@@ -21,7 +21,7 @@ use rustical_dav::{
}, },
}; };
use rustical_ical::CalendarObject; use rustical_ical::CalendarObject;
use rustical_store::{CalendarStore, SubscriptionStore, auth::User}; use rustical_store::{CalendarStore, SubscriptionStore, auth::Principal};
use rustical_xml::{XmlDeserialize, XmlDocument}; use rustical_xml::{XmlDeserialize, XmlDocument};
use sync_collection::handle_sync_collection; use sync_collection::handle_sync_collection;
use tracing::instrument; use tracing::instrument;
@@ -56,7 +56,7 @@ fn objects_response(
path: &str, path: &str,
principal: &str, principal: &str,
puri: &impl PrincipalUri, puri: &impl PrincipalUri,
user: &User, user: &Principal,
prop: &PropfindType<CalendarObjectPropWrapperName>, prop: &PropfindType<CalendarObjectPropWrapperName>,
) -> Result<MultistatusElement<CalendarObjectPropWrapper, String>, Error> { ) -> Result<MultistatusElement<CalendarObjectPropWrapper, String>, Error> {
let mut responses = Vec::new(); let mut responses = Vec::new();
@@ -90,7 +90,7 @@ fn objects_response(
#[instrument(skip(cal_store))] #[instrument(skip(cal_store))]
pub async fn route_report_calendar<C: CalendarStore, S: SubscriptionStore>( pub async fn route_report_calendar<C: CalendarStore, S: SubscriptionStore>(
Path((principal, cal_id)): Path<(String, String)>, Path((principal, cal_id)): Path<(String, String)>,
user: User, user: Principal,
Extension(puri): Extension<CalDavPrincipalUri>, Extension(puri): Extension<CalDavPrincipalUri>,
State(CalendarResourceService { cal_store, .. }): State<CalendarResourceService<C, S>>, State(CalendarResourceService { cal_store, .. }): State<CalendarResourceService<C, S>>,
OriginalUri(uri): OriginalUri, OriginalUri(uri): OriginalUri,

View File

@@ -13,7 +13,7 @@ use rustical_dav::{
}; };
use rustical_store::{ use rustical_store::{
CalendarStore, CalendarStore,
auth::User, auth::Principal,
synctoken::{format_synctoken, parse_synctoken}, synctoken::{format_synctoken, parse_synctoken},
}; };
@@ -21,7 +21,7 @@ pub async fn handle_sync_collection<C: CalendarStore>(
sync_collection: &SyncCollectionRequest<CalendarObjectPropWrapperName>, sync_collection: &SyncCollectionRequest<CalendarObjectPropWrapperName>,
path: &str, path: &str,
puri: &impl PrincipalUri, puri: &impl PrincipalUri,
user: &User, user: &Principal,
principal: &str, principal: &str,
cal_id: &str, cal_id: &str,
cal_store: &C, cal_store: &C,

View File

@@ -9,10 +9,10 @@ use rustical_dav::extensions::{
use rustical_dav::privileges::UserPrivilegeSet; use rustical_dav::privileges::UserPrivilegeSet;
use rustical_dav::resource::{PrincipalUri, Resource, ResourceName}; use rustical_dav::resource::{PrincipalUri, Resource, ResourceName};
use rustical_dav::xml::{HrefElement, Resourcetype, ResourcetypeInner, SupportedReportSet}; use rustical_dav::xml::{HrefElement, Resourcetype, ResourcetypeInner, SupportedReportSet};
use rustical_dav_push::DavPushExtension; use rustical_dav_push::{DavPushExtension, DavPushExtensionProp};
use rustical_ical::CalDateTime; use rustical_ical::CalDateTime;
use rustical_store::Calendar; use rustical_store::Calendar;
use rustical_store::auth::User; use rustical_store::auth::Principal;
use rustical_xml::{EnumVariants, PropName}; use rustical_xml::{EnumVariants, PropName};
use rustical_xml::{XmlDeserialize, XmlSerialize}; use rustical_xml::{XmlDeserialize, XmlSerialize};
use std::str::FromStr; use std::str::FromStr;
@@ -58,7 +58,7 @@ pub enum CalendarProp {
pub enum CalendarPropWrapper { pub enum CalendarPropWrapper {
Calendar(CalendarProp), Calendar(CalendarProp),
SyncToken(SyncTokenExtensionProp), SyncToken(SyncTokenExtensionProp),
// DavPush(DavPushExtensionProp), DavPush(DavPushExtensionProp),
Common(CommonPropertiesProp), Common(CommonPropertiesProp),
} }
@@ -95,9 +95,11 @@ impl DavPushExtension for CalendarResource {
impl Resource for CalendarResource { impl Resource for CalendarResource {
type Prop = CalendarPropWrapper; type Prop = CalendarPropWrapper;
type Error = Error; type Error = Error;
type Principal = User; type Principal = Principal;
const IS_COLLECTION: bool = true; fn is_collection(&self) -> bool {
true
}
fn get_resourcetype(&self) -> Resourcetype { fn get_resourcetype(&self) -> Resourcetype {
if self.cal.subscription_url.is_none() { if self.cal.subscription_url.is_none() {
@@ -119,7 +121,7 @@ impl Resource for CalendarResource {
fn get_prop( fn get_prop(
&self, &self,
puri: &impl PrincipalUri, puri: &impl PrincipalUri,
user: &User, user: &Principal,
prop: &CalendarPropWrapperName, prop: &CalendarPropWrapperName,
) -> Result<Self::Prop, Self::Error> { ) -> Result<Self::Prop, Self::Error> {
Ok(match prop { Ok(match prop {
@@ -166,9 +168,9 @@ impl Resource for CalendarResource {
CalendarPropWrapperName::SyncToken(prop) => { CalendarPropWrapperName::SyncToken(prop) => {
CalendarPropWrapper::SyncToken(SyncTokenExtension::get_prop(self, prop)?) CalendarPropWrapper::SyncToken(SyncTokenExtension::get_prop(self, prop)?)
} }
// CalendarPropWrapperName::DavPush(prop) => { CalendarPropWrapperName::DavPush(prop) => {
// CalendarPropWrapper::DavPush(DavPushExtension::get_prop(self, prop)?) CalendarPropWrapper::DavPush(DavPushExtension::get_prop(self, prop)?)
// } }
CalendarPropWrapperName::Common(prop) => CalendarPropWrapper::Common( CalendarPropWrapperName::Common(prop) => CalendarPropWrapper::Common(
CommonPropertiesExtension::get_prop(self, puri, user, prop)?, CommonPropertiesExtension::get_prop(self, puri, user, prop)?,
), ),
@@ -226,7 +228,7 @@ impl Resource for CalendarResource {
CalendarProp::MaxDateTime(_) => Err(rustical_dav::Error::PropReadOnly), CalendarProp::MaxDateTime(_) => Err(rustical_dav::Error::PropReadOnly),
}, },
CalendarPropWrapper::SyncToken(prop) => SyncTokenExtension::set_prop(self, prop), CalendarPropWrapper::SyncToken(prop) => SyncTokenExtension::set_prop(self, prop),
// CalendarPropWrapper::DavPush(prop) => DavPushExtension::set_prop(self, prop), CalendarPropWrapper::DavPush(prop) => DavPushExtension::set_prop(self, prop),
CalendarPropWrapper::Common(prop) => CommonPropertiesExtension::set_prop(self, prop), CalendarPropWrapper::Common(prop) => CommonPropertiesExtension::set_prop(self, prop),
} }
} }
@@ -270,7 +272,7 @@ impl Resource for CalendarResource {
CalendarPropName::MaxDateTime => Err(rustical_dav::Error::PropReadOnly), CalendarPropName::MaxDateTime => Err(rustical_dav::Error::PropReadOnly),
}, },
CalendarPropWrapperName::SyncToken(prop) => SyncTokenExtension::remove_prop(self, prop), CalendarPropWrapperName::SyncToken(prop) => SyncTokenExtension::remove_prop(self, prop),
// CalendarPropWrapperName::DavPush(prop) => DavPushExtension::remove_prop(self, prop), CalendarPropWrapperName::DavPush(prop) => DavPushExtension::remove_prop(self, prop),
CalendarPropWrapperName::Common(prop) => { CalendarPropWrapperName::Common(prop) => {
CommonPropertiesExtension::remove_prop(self, prop) CommonPropertiesExtension::remove_prop(self, prop)
} }
@@ -289,7 +291,7 @@ impl Resource for CalendarResource {
Some(&self.cal.principal) Some(&self.cal.principal)
} }
fn get_user_privileges(&self, user: &User) -> Result<UserPrivilegeSet, Self::Error> { fn get_user_privileges(&self, user: &Principal) -> Result<UserPrivilegeSet, Self::Error> {
if self.cal.subscription_url.is_some() || self.read_only { if self.cal.subscription_url.is_some() || self.read_only {
return Ok(UserPrivilegeSet::owner_read( return Ok(UserPrivilegeSet::owner_read(
user.is_principal(&self.cal.principal), user.is_principal(&self.cal.principal),

View File

@@ -1,5 +1,6 @@
use crate::calendar::methods::get::route_get; use crate::calendar::methods::get::route_get;
use crate::calendar::methods::mkcalendar::route_mkcalendar; use crate::calendar::methods::mkcalendar::route_mkcalendar;
use crate::calendar::methods::post::route_post;
use crate::calendar::methods::report::route_report_calendar; use crate::calendar::methods::report::route_report_calendar;
use crate::calendar::resource::CalendarResource; use crate::calendar::resource::CalendarResource;
use crate::calendar_object::CalendarObjectResourceService; use crate::calendar_object::CalendarObjectResourceService;
@@ -12,7 +13,7 @@ use axum::handler::Handler;
use axum::response::Response; use axum::response::Response;
use futures_util::future::BoxFuture; use futures_util::future::BoxFuture;
use rustical_dav::resource::{AxumMethods, ResourceService}; use rustical_dav::resource::{AxumMethods, ResourceService};
use rustical_store::auth::User; use rustical_store::auth::Principal;
use rustical_store::{CalendarStore, SubscriptionStore}; use rustical_store::{CalendarStore, SubscriptionStore};
use std::convert::Infallible; use std::convert::Infallible;
use std::sync::Arc; use std::sync::Arc;
@@ -47,10 +48,10 @@ impl<C: CalendarStore, S: SubscriptionStore> ResourceService for CalendarResourc
type PathComponents = (String, String); // principal, calendar_id type PathComponents = (String, String); // principal, calendar_id
type Resource = CalendarResource; type Resource = CalendarResource;
type Error = Error; type Error = Error;
type Principal = User; type Principal = Principal;
type PrincipalUri = CalDavPrincipalUri; type PrincipalUri = CalDavPrincipalUri;
const DAV_HEADER: &str = "1, 3, access-control, calendar-access, calendar-proxy"; const DAV_HEADER: &str = "1, 3, access-control, calendar-access, calendar-proxy, webdav-push";
async fn get_resource( async fn get_resource(
&self, &self,
@@ -126,6 +127,13 @@ impl<C: CalendarStore, S: SubscriptionStore> AxumMethods for CalendarResourceSer
}) })
} }
fn post() -> Option<fn(Self, Request) -> BoxFuture<'static, Result<Response, Infallible>>> {
Some(|state, req| {
let mut service = Handler::with_state(route_post::<C, S>, state);
Box::pin(Service::call(&mut service, req))
})
}
fn mkcalendar() -> Option<fn(Self, Request) -> BoxFuture<'static, Result<Response, Infallible>>> fn mkcalendar() -> Option<fn(Self, Request) -> BoxFuture<'static, Result<Response, Infallible>>>
{ {
Some(|state, req| { Some(|state, req| {

View File

@@ -9,7 +9,7 @@ use headers::{ContentType, ETag, HeaderMapExt, IfNoneMatch};
use http::{HeaderMap, StatusCode}; use http::{HeaderMap, StatusCode};
use rustical_ical::CalendarObject; use rustical_ical::CalendarObject;
use rustical_store::CalendarStore; use rustical_store::CalendarStore;
use rustical_store::auth::User; use rustical_store::auth::Principal;
use std::str::FromStr; use std::str::FromStr;
use tracing::instrument; use tracing::instrument;
@@ -21,7 +21,7 @@ pub async fn get_event<C: CalendarStore>(
object_id, object_id,
}): Path<CalendarObjectPathComponents>, }): Path<CalendarObjectPathComponents>,
State(CalendarObjectResourceService { cal_store }): State<CalendarObjectResourceService<C>>, State(CalendarObjectResourceService { cal_store }): State<CalendarObjectResourceService<C>>,
user: User, user: Principal,
) -> Result<Response, Error> { ) -> Result<Response, Error> {
if !user.is_principal(&principal) { if !user.is_principal(&principal) {
return Err(crate::Error::Unauthorized); return Err(crate::Error::Unauthorized);
@@ -33,7 +33,7 @@ pub async fn get_event<C: CalendarStore>(
} }
let event = cal_store let event = cal_store
.get_object(&principal, &calendar_id, &object_id) .get_object(&principal, &calendar_id, &object_id, false)
.await?; .await?;
let mut resp = Response::builder().status(StatusCode::OK); let mut resp = Response::builder().status(StatusCode::OK);
@@ -51,7 +51,7 @@ pub async fn put_event<C: CalendarStore>(
object_id, object_id,
}): Path<CalendarObjectPathComponents>, }): Path<CalendarObjectPathComponents>,
State(CalendarObjectResourceService { cal_store }): State<CalendarObjectResourceService<C>>, State(CalendarObjectResourceService { cal_store }): State<CalendarObjectResourceService<C>>,
user: User, user: Principal,
mut if_none_match: Option<TypedHeader<IfNoneMatch>>, mut if_none_match: Option<TypedHeader<IfNoneMatch>>,
header_map: HeaderMap, header_map: HeaderMap,
body: String, body: String,

View File

@@ -8,7 +8,7 @@ use rustical_dav::{
xml::Resourcetype, xml::Resourcetype,
}; };
use rustical_ical::CalendarObject; use rustical_ical::CalendarObject;
use rustical_store::auth::User; use rustical_store::auth::Principal;
#[derive(Clone, From, Into)] #[derive(Clone, From, Into)]
pub struct CalendarObjectResource { pub struct CalendarObjectResource {
@@ -25,9 +25,11 @@ impl ResourceName for CalendarObjectResource {
impl Resource for CalendarObjectResource { impl Resource for CalendarObjectResource {
type Prop = CalendarObjectPropWrapper; type Prop = CalendarObjectPropWrapper;
type Error = Error; type Error = Error;
type Principal = User; type Principal = Principal;
const IS_COLLECTION: bool = false; fn is_collection(&self) -> bool {
false
}
fn get_resourcetype(&self) -> Resourcetype { fn get_resourcetype(&self) -> Resourcetype {
Resourcetype(&[]) Resourcetype(&[])
@@ -36,7 +38,7 @@ impl Resource for CalendarObjectResource {
fn get_prop( fn get_prop(
&self, &self,
puri: &impl PrincipalUri, puri: &impl PrincipalUri,
user: &User, user: &Principal,
prop: &CalendarObjectPropWrapperName, prop: &CalendarObjectPropWrapperName,
) -> Result<Self::Prop, Self::Error> { ) -> Result<Self::Prop, Self::Error> {
Ok(match prop { Ok(match prop {
@@ -79,7 +81,7 @@ impl Resource for CalendarObjectResource {
Some(self.object.get_etag()) Some(self.object.get_etag())
} }
fn get_user_privileges(&self, user: &User) -> Result<UserPrivilegeSet, Self::Error> { fn get_user_privileges(&self, user: &Principal) -> Result<UserPrivilegeSet, Self::Error> {
Ok(UserPrivilegeSet::owner_only( Ok(UserPrivilegeSet::owner_only(
user.is_principal(&self.principal), user.is_principal(&self.principal),
)) ))

View File

@@ -9,7 +9,7 @@ use async_trait::async_trait;
use axum::{extract::Request, handler::Handler, response::Response}; use axum::{extract::Request, handler::Handler, response::Response};
use futures_util::future::BoxFuture; use futures_util::future::BoxFuture;
use rustical_dav::resource::{AxumMethods, ResourceService}; use rustical_dav::resource::{AxumMethods, ResourceService};
use rustical_store::{CalendarStore, auth::User}; use rustical_store::{CalendarStore, auth::Principal};
use serde::{Deserialize, Deserializer}; use serde::{Deserialize, Deserializer};
use std::{convert::Infallible, sync::Arc}; use std::{convert::Infallible, sync::Arc};
use tower::Service; use tower::Service;
@@ -46,7 +46,7 @@ impl<C: CalendarStore> ResourceService for CalendarObjectResourceService<C> {
type Resource = CalendarObjectResource; type Resource = CalendarObjectResource;
type MemberType = CalendarObjectResource; type MemberType = CalendarObjectResource;
type Error = Error; type Error = Error;
type Principal = User; type Principal = Principal;
type PrincipalUri = CalDavPrincipalUri; type PrincipalUri = CalDavPrincipalUri;
const DAV_HEADER: &str = "1, 3, access-control, calendar-access"; const DAV_HEADER: &str = "1, 3, access-control, calendar-access";
@@ -61,7 +61,7 @@ impl<C: CalendarStore> ResourceService for CalendarObjectResourceService<C> {
) -> Result<Self::Resource, Self::Error> { ) -> Result<Self::Resource, Self::Error> {
let object = self let object = self
.cal_store .cal_store
.get_object(principal, calendar_id, object_id) .get_object(principal, calendar_id, object_id, false)
.await?; .await?;
Ok(CalendarObjectResource { Ok(CalendarObjectResource {
object, object,

View File

@@ -6,7 +6,7 @@ use principal::PrincipalResourceService;
use rustical_dav::resource::{PrincipalUri, ResourceService}; use rustical_dav::resource::{PrincipalUri, ResourceService};
use rustical_dav::resources::RootResourceService; use rustical_dav::resources::RootResourceService;
use rustical_store::auth::middleware::AuthenticationLayer; use rustical_store::auth::middleware::AuthenticationLayer;
use rustical_store::auth::{AuthenticationProvider, User}; use rustical_store::auth::{AuthenticationProvider, Principal};
use rustical_store::{CalendarStore, SubscriptionStore}; use rustical_store::{CalendarStore, SubscriptionStore};
use std::sync::Arc; use std::sync::Arc;
@@ -14,7 +14,6 @@ pub mod calendar;
pub mod calendar_object; pub mod calendar_object;
pub mod error; pub mod error;
pub mod principal; pub mod principal;
// mod subscription;
pub use error::Error; pub use error::Error;
@@ -45,7 +44,7 @@ pub fn caldav_router<AP: AuthenticationProvider, C: CalendarStore, S: Subscripti
Router::new() Router::new()
.nest( .nest(
prefix, prefix,
RootResourceService::<_, User, CalDavPrincipalUri>::new(principal_service.clone()) RootResourceService::<_, Principal, CalDavPrincipalUri>::new(principal_service.clone())
.axum_router() .axum_router()
.layer(AuthenticationLayer::new(auth_provider)) .layer(AuthenticationLayer::new(auth_provider))
.layer(Extension(CalDavPrincipalUri(prefix))), .layer(Extension(CalDavPrincipalUri(prefix))),

View File

@@ -5,7 +5,7 @@ use rustical_dav::resource::{PrincipalUri, Resource, ResourceName};
use rustical_dav::xml::{ use rustical_dav::xml::{
GroupMemberSet, GroupMembership, Resourcetype, ResourcetypeInner, SupportedReportSet, GroupMemberSet, GroupMembership, Resourcetype, ResourcetypeInner, SupportedReportSet,
}; };
use rustical_store::auth::User; use rustical_store::auth::Principal;
mod service; mod service;
pub use service::*; pub use service::*;
@@ -14,7 +14,7 @@ pub use prop::*;
#[derive(Clone)] #[derive(Clone)]
pub struct PrincipalResource { pub struct PrincipalResource {
principal: User, principal: Principal,
members: Vec<String>, members: Vec<String>,
} }
@@ -27,26 +27,28 @@ impl ResourceName for PrincipalResource {
impl Resource for PrincipalResource { impl Resource for PrincipalResource {
type Prop = PrincipalPropWrapper; type Prop = PrincipalPropWrapper;
type Error = Error; type Error = Error;
type Principal = User; type Principal = Principal;
const IS_COLLECTION: bool = true; fn is_collection(&self) -> bool {
true
}
fn get_resourcetype(&self) -> Resourcetype { fn get_resourcetype(&self) -> Resourcetype {
Resourcetype(&[ Resourcetype(&[
ResourcetypeInner(Some(rustical_dav::namespace::NS_DAV), "collection"), ResourcetypeInner(Some(rustical_dav::namespace::NS_DAV), "collection"),
ResourcetypeInner(Some(rustical_dav::namespace::NS_DAV), "principal"), ResourcetypeInner(Some(rustical_dav::namespace::NS_DAV), "principal"),
// https://github.com/apple/ccs-calendarserver/blob/13c706b985fb728b9aab42dc0fef85aae21921c3/doc/Extensions/caldav-proxy.txt // https://github.com/apple/ccs-calendarserver/blob/13c706b985fb728b9aab42dc0fef85aae21921c3/doc/Extensions/caldav-proxy.txt
ResourcetypeInner( // ResourcetypeInner(
Some(rustical_dav::namespace::NS_CALENDARSERVER), // Some(rustical_dav::namespace::NS_CALENDARSERVER),
"calendar-proxy-write", // "calendar-proxy-write",
), // ),
]) ])
} }
fn get_prop( fn get_prop(
&self, &self,
puri: &impl PrincipalUri, puri: &impl PrincipalUri,
user: &User, user: &Principal,
prop: &PrincipalPropWrapperName, prop: &PrincipalPropWrapperName,
) -> Result<Self::Prop, Self::Error> { ) -> Result<Self::Prop, Self::Error> {
let principal_url = puri.principal_uri(&self.principal.id); let principal_url = puri.principal_uri(&self.principal.id);
@@ -111,7 +113,7 @@ impl Resource for PrincipalResource {
Some(&self.principal.id) Some(&self.principal.id)
} }
fn get_user_privileges(&self, user: &User) -> Result<UserPrivilegeSet, Self::Error> { fn get_user_privileges(&self, user: &Principal) -> Result<UserPrivilegeSet, Self::Error> {
Ok(UserPrivilegeSet::owner_read( Ok(UserPrivilegeSet::owner_read(
user.is_principal(&self.principal.id), user.is_principal(&self.principal.id),
)) ))

View File

@@ -2,7 +2,7 @@ use rustical_dav::{
extensions::CommonPropertiesProp, extensions::CommonPropertiesProp,
xml::{GroupMemberSet, GroupMembership, HrefElement, SupportedReportSet}, xml::{GroupMemberSet, GroupMembership, HrefElement, SupportedReportSet},
}; };
use rustical_store::auth::user::PrincipalType; use rustical_store::auth::PrincipalType;
use rustical_xml::{EnumVariants, PropName, XmlDeserialize, XmlSerialize}; use rustical_xml::{EnumVariants, PropName, XmlDeserialize, XmlSerialize};
use strum_macros::VariantArray; use strum_macros::VariantArray;

View File

@@ -5,7 +5,7 @@ use crate::{CalDavPrincipalUri, Error};
use async_trait::async_trait; use async_trait::async_trait;
use axum::Router; use axum::Router;
use rustical_dav::resource::{AxumMethods, ResourceService}; use rustical_dav::resource::{AxumMethods, ResourceService};
use rustical_store::auth::{AuthenticationProvider, User}; use rustical_store::auth::{AuthenticationProvider, Principal};
use rustical_store::{CalendarStore, SubscriptionStore}; use rustical_store::{CalendarStore, SubscriptionStore};
use std::sync::Arc; use std::sync::Arc;
@@ -40,7 +40,7 @@ impl<AP: AuthenticationProvider, S: SubscriptionStore, CS: CalendarStore> Resour
type MemberType = CalendarResource; type MemberType = CalendarResource;
type Resource = PrincipalResource; type Resource = PrincipalResource;
type Error = Error; type Error = Error;
type Principal = User; type Principal = Principal;
type PrincipalUri = CalDavPrincipalUri; type PrincipalUri = CalDavPrincipalUri;
const DAV_HEADER: &str = "1, 3, access-control, calendar-access, calendar-proxy"; const DAV_HEADER: &str = "1, 3, access-control, calendar-access, calendar-proxy";

View File

@@ -1,33 +0,0 @@
use std::sync::Arc;
use actix_web::{
HttpResponse,
web::{self, Data, Path},
};
use rustical_dav::xml::multistatus::PropstatElement;
use rustical_store::SubscriptionStore;
use rustical_xml::{XmlRootTag, XmlSerialize};
use crate::calendar::resource::CalendarProp;
async fn handle_delete<S: SubscriptionStore>(
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>(sub_store: Arc<S>) -> actix_web::Resource {
web::resource("/subscription/{id}")
.app_data(Data::from(sub_store))
.name("subscription")
.delete(handle_delete::<S>)
}
#[derive(XmlSerialize, XmlRootTag)]
#[xml(root = b"push-message", ns = "rustical_dav::namespace::NS_DAVPUSH")]
pub struct PushMessage {
propstat: PropstatElement<CalendarProp>,
}

View File

@@ -12,7 +12,7 @@ use rustical_dav::privileges::UserPrivilege;
use rustical_dav::resource::Resource; use rustical_dav::resource::Resource;
use rustical_ical::AddressObject; use rustical_ical::AddressObject;
use rustical_store::AddressbookStore; use rustical_store::AddressbookStore;
use rustical_store::auth::User; use rustical_store::auth::Principal;
use std::str::FromStr; use std::str::FromStr;
use tracing::instrument; use tracing::instrument;
@@ -24,7 +24,7 @@ pub async fn get_object<AS: AddressbookStore>(
object_id, object_id,
}): Path<AddressObjectPathComponents>, }): Path<AddressObjectPathComponents>,
State(AddressObjectResourceService { addr_store }): State<AddressObjectResourceService<AS>>, State(AddressObjectResourceService { addr_store }): State<AddressObjectResourceService<AS>>,
user: User, user: Principal,
) -> Result<Response, Error> { ) -> Result<Response, Error> {
if !user.is_principal(&principal) { if !user.is_principal(&principal) {
return Err(Error::Unauthorized); return Err(Error::Unauthorized);
@@ -60,7 +60,7 @@ pub async fn put_object<AS: AddressbookStore>(
object_id, object_id,
}): Path<AddressObjectPathComponents>, }): Path<AddressObjectPathComponents>,
State(AddressObjectResourceService { addr_store }): State<AddressObjectResourceService<AS>>, State(AddressObjectResourceService { addr_store }): State<AddressObjectResourceService<AS>>,
user: User, user: Principal,
mut if_none_match: Option<TypedHeader<IfNoneMatch>>, mut if_none_match: Option<TypedHeader<IfNoneMatch>>,
header_map: HeaderMap, header_map: HeaderMap,
body: String, body: String,

View File

@@ -13,7 +13,7 @@ use rustical_dav::{
xml::Resourcetype, xml::Resourcetype,
}; };
use rustical_ical::AddressObject; use rustical_ical::AddressObject;
use rustical_store::auth::User; use rustical_store::auth::Principal;
#[derive(Clone, From, Into)] #[derive(Clone, From, Into)]
pub struct AddressObjectResource { pub struct AddressObjectResource {
@@ -30,9 +30,11 @@ impl ResourceName for AddressObjectResource {
impl Resource for AddressObjectResource { impl Resource for AddressObjectResource {
type Prop = AddressObjectPropWrapper; type Prop = AddressObjectPropWrapper;
type Error = Error; type Error = Error;
type Principal = User; type Principal = Principal;
const IS_COLLECTION: bool = false; fn is_collection(&self) -> bool {
false
}
fn get_resourcetype(&self) -> Resourcetype { fn get_resourcetype(&self) -> Resourcetype {
Resourcetype(&[]) Resourcetype(&[])
@@ -41,7 +43,7 @@ impl Resource for AddressObjectResource {
fn get_prop( fn get_prop(
&self, &self,
puri: &impl PrincipalUri, puri: &impl PrincipalUri,
user: &User, user: &Principal,
prop: &AddressObjectPropWrapperName, prop: &AddressObjectPropWrapperName,
) -> Result<Self::Prop, Self::Error> { ) -> Result<Self::Prop, Self::Error> {
Ok(match prop { Ok(match prop {
@@ -76,7 +78,7 @@ impl Resource for AddressObjectResource {
Some(self.object.get_etag()) Some(self.object.get_etag())
} }
fn get_user_privileges(&self, user: &User) -> Result<UserPrivilegeSet, Self::Error> { fn get_user_privileges(&self, user: &Principal) -> Result<UserPrivilegeSet, Self::Error> {
Ok(UserPrivilegeSet::owner_only( Ok(UserPrivilegeSet::owner_only(
user.is_principal(&self.principal), user.is_principal(&self.principal),
)) ))

View File

@@ -5,7 +5,7 @@ use axum::{extract::Request, handler::Handler, response::Response};
use derive_more::derive::Constructor; use derive_more::derive::Constructor;
use futures_util::future::BoxFuture; use futures_util::future::BoxFuture;
use rustical_dav::resource::{AxumMethods, ResourceService}; use rustical_dav::resource::{AxumMethods, ResourceService};
use rustical_store::{AddressbookStore, auth::User}; use rustical_store::{AddressbookStore, auth::Principal};
use serde::{Deserialize, Deserializer}; use serde::{Deserialize, Deserializer};
use std::{convert::Infallible, sync::Arc}; use std::{convert::Infallible, sync::Arc};
use tower::Service; use tower::Service;
@@ -37,7 +37,7 @@ impl<AS: AddressbookStore> ResourceService for AddressObjectResourceService<AS>
type Resource = AddressObjectResource; type Resource = AddressObjectResource;
type MemberType = AddressObjectResource; type MemberType = AddressObjectResource;
type Error = Error; type Error = Error;
type Principal = User; type Principal = Principal;
type PrincipalUri = CardDavPrincipalUri; type PrincipalUri = CardDavPrincipalUri;
const DAV_HEADER: &str = "1, 3, access-control, addressbook"; const DAV_HEADER: &str = "1, 3, access-control, addressbook";

View File

@@ -10,7 +10,7 @@ use percent_encoding::{CONTROLS, utf8_percent_encode};
use rustical_dav::privileges::UserPrivilege; use rustical_dav::privileges::UserPrivilege;
use rustical_dav::resource::Resource; use rustical_dav::resource::Resource;
use rustical_ical::AddressObject; use rustical_ical::AddressObject;
use rustical_store::auth::User; use rustical_store::auth::Principal;
use rustical_store::{AddressbookStore, SubscriptionStore}; use rustical_store::{AddressbookStore, SubscriptionStore};
use std::str::FromStr; use std::str::FromStr;
use tracing::instrument; use tracing::instrument;
@@ -19,7 +19,7 @@ use tracing::instrument;
pub async fn route_get<AS: AddressbookStore, S: SubscriptionStore>( pub async fn route_get<AS: AddressbookStore, S: SubscriptionStore>(
Path((principal, addressbook_id)): Path<(String, String)>, Path((principal, addressbook_id)): Path<(String, String)>,
State(AddressbookResourceService { addr_store, .. }): State<AddressbookResourceService<AS, S>>, State(AddressbookResourceService { addr_store, .. }): State<AddressbookResourceService<AS, S>>,
user: User, user: Principal,
) -> Result<Response, Error> { ) -> Result<Response, Error> {
if !user.is_principal(&principal) { if !user.is_principal(&principal) {
return Err(Error::Unauthorized); return Err(Error::Unauthorized);

View File

@@ -4,7 +4,7 @@ use axum::{
response::{IntoResponse, Response}, response::{IntoResponse, Response},
}; };
use http::StatusCode; use http::StatusCode;
use rustical_store::{Addressbook, AddressbookStore, SubscriptionStore, auth::User}; use rustical_store::{Addressbook, AddressbookStore, SubscriptionStore, auth::Principal};
use rustical_xml::{XmlDeserialize, XmlDocument, XmlRootTag}; use rustical_xml::{XmlDeserialize, XmlDocument, XmlRootTag};
use tracing::instrument; use tracing::instrument;
@@ -44,7 +44,7 @@ struct MkcolRequest {
#[instrument(skip(addr_store))] #[instrument(skip(addr_store))]
pub async fn route_mkcol<AS: AddressbookStore, S: SubscriptionStore>( pub async fn route_mkcol<AS: AddressbookStore, S: SubscriptionStore>(
Path((principal, addressbook_id)): Path<(String, String)>, Path((principal, addressbook_id)): Path<(String, String)>,
user: User, user: Principal,
State(AddressbookResourceService { addr_store, .. }): State<AddressbookResourceService<AS, S>>, State(AddressbookResourceService { addr_store, .. }): State<AddressbookResourceService<AS, S>>,
body: String, body: String,
) -> Result<Response, Error> { ) -> Result<Response, Error> {

View File

@@ -1,5 +1,5 @@
pub mod mkcol;
// pub mod post;
pub mod get; pub mod get;
pub mod mkcol;
pub mod post;
pub mod put; pub mod put;
pub mod report; pub mod report;

View File

@@ -1,33 +1,40 @@
use crate::Error; use crate::Error;
use crate::addressbook::resource::AddressbookResourceService; use crate::addressbook::AddressbookResourceService;
use actix_web::http::header; use crate::addressbook::resource::AddressbookResource;
use actix_web::web::{Data, Path}; use axum::extract::{Path, State};
use actix_web::{HttpRequest, HttpResponse}; use axum::response::{IntoResponse, Response};
use http::{HeaderMap, HeaderValue, StatusCode, header};
use rustical_dav::privileges::UserPrivilege;
use rustical_dav::resource::Resource;
use rustical_dav_push::register::PushRegister; use rustical_dav_push::register::PushRegister;
use rustical_store::auth::User; use rustical_store::auth::Principal;
use rustical_store::{AddressbookStore, Subscription, SubscriptionStore}; use rustical_store::{AddressbookStore, Subscription, SubscriptionStore};
use rustical_xml::XmlDocument; use rustical_xml::XmlDocument;
use tracing::instrument; use tracing::instrument;
use tracing_actix_web::RootSpan;
#[instrument(parent = root_span.id(), skip(resource_service, root_span, req))] #[instrument(skip(resource_service))]
pub async fn route_post<A: AddressbookStore, S: SubscriptionStore>( pub async fn route_post<AS: AddressbookStore, S: SubscriptionStore>(
path: Path<(String, String)>, Path((principal, addr_id)): Path<(String, String)>,
user: Principal,
State(resource_service): State<AddressbookResourceService<AS, S>>,
body: String, body: String,
user: User, ) -> Result<Response, Error> {
resource_service: Data<AddressbookResourceService<A, S>>,
root_span: RootSpan,
req: HttpRequest,
) -> Result<HttpResponse, Error> {
let (principal, addressbook_id) = path.into_inner();
if !user.is_principal(&principal) { if !user.is_principal(&principal) {
return Err(Error::Unauthorized); return Err(Error::Unauthorized);
} }
let addressbook = resource_service let addressbook = resource_service
.addr_store .addr_store
.get_addressbook(&principal, &addressbook_id, false) .get_addressbook(&principal, &addr_id, false)
.await?; .await?;
let addressbook_resource = AddressbookResource(addressbook);
if !addressbook_resource
.get_user_privileges(&user)?
.has(&UserPrivilege::Read)
{
return Err(Error::Unauthorized);
}
let request = PushRegister::parse_str(&body)?; let request = PushRegister::parse_str(&body)?;
let sub_id = uuid::Uuid::new_v4().to_string(); let sub_id = uuid::Uuid::new_v4().to_string();
@@ -44,7 +51,7 @@ pub async fn route_post<A: AddressbookStore, S: SubscriptionStore>(
.web_push_subscription .web_push_subscription
.push_resource .push_resource
.to_owned(), .to_owned(),
topic: addressbook.push_topic, topic: addressbook_resource.0.push_topic,
expiration: expires.naive_local(), expiration: expires.naive_local(),
public_key: request public_key: request
.subscription .subscription
@@ -63,13 +70,17 @@ pub async fn route_post<A: AddressbookStore, S: SubscriptionStore>(
.upsert_subscription(subscription) .upsert_subscription(subscription)
.await?; .await?;
let location = req // TODO: make nicer
.resource_map() let location = format!("/push_subscription/{sub_id}");
.url_for(&req, "subscription", &[sub_id]) Ok((
.unwrap(); StatusCode::CREATED,
HeaderMap::from_iter([
Ok(HttpResponse::Created() (header::LOCATION, HeaderValue::from_str(&location).unwrap()),
.append_header((header::LOCATION, location.to_string())) (
.append_header((header::EXPIRES, expires.to_rfc2822())) header::EXPIRES,
.finish()) HeaderValue::from_str(&expires.to_rfc2822()).unwrap(),
),
]),
)
.into_response())
} }

View File

@@ -9,14 +9,14 @@ use http::StatusCode;
use ical::VcardParser; use ical::VcardParser;
use rustical_ical::AddressObject; use rustical_ical::AddressObject;
use rustical_store::Addressbook; use rustical_store::Addressbook;
use rustical_store::{AddressbookStore, SubscriptionStore, auth::User}; use rustical_store::{AddressbookStore, SubscriptionStore, auth::Principal};
use tracing::instrument; use tracing::instrument;
#[instrument(skip(addr_store))] #[instrument(skip(addr_store))]
pub async fn route_put<AS: AddressbookStore, S: SubscriptionStore>( pub async fn route_put<AS: AddressbookStore, S: SubscriptionStore>(
Path((principal, addressbook_id)): Path<(String, String)>, Path((principal, addressbook_id)): Path<(String, String)>,
State(AddressbookResourceService { addr_store, .. }): State<AddressbookResourceService<AS, S>>, State(AddressbookResourceService { addr_store, .. }): State<AddressbookResourceService<AS, S>>,
user: User, user: Principal,
body: String, body: String,
) -> Result<Response, Error> { ) -> Result<Response, Error> {
if !user.is_principal(&principal) { if !user.is_principal(&principal) {

View File

@@ -10,7 +10,7 @@ use rustical_dav::{
xml::{MultistatusElement, PropfindType, multistatus::ResponseElement}, xml::{MultistatusElement, PropfindType, multistatus::ResponseElement},
}; };
use rustical_ical::AddressObject; use rustical_ical::AddressObject;
use rustical_store::{AddressbookStore, auth::User}; use rustical_store::{AddressbookStore, auth::Principal};
use rustical_xml::XmlDeserialize; use rustical_xml::XmlDeserialize;
#[derive(XmlDeserialize, Clone, Debug, PartialEq)] #[derive(XmlDeserialize, Clone, Debug, PartialEq)]
@@ -63,7 +63,7 @@ pub async fn handle_addressbook_multiget<AS: AddressbookStore>(
prop: &PropfindType<AddressObjectPropWrapperName>, prop: &PropfindType<AddressObjectPropWrapperName>,
path: &str, path: &str,
puri: &impl PrincipalUri, puri: &impl PrincipalUri,
user: &User, user: &Principal,
principal: &str, principal: &str,
cal_id: &str, cal_id: &str,
addr_store: &AS, addr_store: &AS,

View File

@@ -9,7 +9,7 @@ use axum::{
response::IntoResponse, response::IntoResponse,
}; };
use rustical_dav::xml::{PropfindType, sync_collection::SyncCollectionRequest}; use rustical_dav::xml::{PropfindType, sync_collection::SyncCollectionRequest};
use rustical_store::{AddressbookStore, SubscriptionStore, auth::User}; use rustical_store::{AddressbookStore, SubscriptionStore, auth::Principal};
use rustical_xml::{XmlDeserialize, XmlDocument}; use rustical_xml::{XmlDeserialize, XmlDocument};
use sync_collection::handle_sync_collection; use sync_collection::handle_sync_collection;
use tracing::instrument; use tracing::instrument;
@@ -37,7 +37,7 @@ impl ReportRequest {
#[instrument(skip(addr_store))] #[instrument(skip(addr_store))]
pub async fn route_report_addressbook<AS: AddressbookStore, S: SubscriptionStore>( pub async fn route_report_addressbook<AS: AddressbookStore, S: SubscriptionStore>(
Path((principal, addressbook_id)): Path<(String, String)>, Path((principal, addressbook_id)): Path<(String, String)>,
user: User, user: Principal,
OriginalUri(uri): OriginalUri, OriginalUri(uri): OriginalUri,
Extension(puri): Extension<CardDavPrincipalUri>, Extension(puri): Extension<CardDavPrincipalUri>,
State(AddressbookResourceService { addr_store, .. }): State<AddressbookResourceService<AS, S>>, State(AddressbookResourceService { addr_store, .. }): State<AddressbookResourceService<AS, S>>,

View File

@@ -13,7 +13,7 @@ use rustical_dav::{
}; };
use rustical_store::{ use rustical_store::{
AddressbookStore, AddressbookStore,
auth::User, auth::Principal,
synctoken::{format_synctoken, parse_synctoken}, synctoken::{format_synctoken, parse_synctoken},
}; };
@@ -21,7 +21,7 @@ pub async fn handle_sync_collection<AS: AddressbookStore>(
sync_collection: &SyncCollectionRequest<AddressObjectPropWrapperName>, sync_collection: &SyncCollectionRequest<AddressObjectPropWrapperName>,
path: &str, path: &str,
puri: &impl PrincipalUri, puri: &impl PrincipalUri,
user: &User, user: &Principal,
principal: &str, principal: &str,
addressbook_id: &str, addressbook_id: &str,
addr_store: &AS, addr_store: &AS,

View File

@@ -10,7 +10,7 @@ use rustical_dav::resource::{PrincipalUri, Resource, ResourceName};
use rustical_dav::xml::{Resourcetype, ResourcetypeInner, SupportedReportSet}; use rustical_dav::xml::{Resourcetype, ResourcetypeInner, SupportedReportSet};
use rustical_dav_push::DavPushExtension; use rustical_dav_push::DavPushExtension;
use rustical_store::Addressbook; use rustical_store::Addressbook;
use rustical_store::auth::User; use rustical_store::auth::Principal;
#[derive(Clone, Debug, From, Into)] #[derive(Clone, Debug, From, Into)]
pub struct AddressbookResource(pub(crate) Addressbook); pub struct AddressbookResource(pub(crate) Addressbook);
@@ -36,9 +36,11 @@ impl DavPushExtension for AddressbookResource {
impl Resource for AddressbookResource { impl Resource for AddressbookResource {
type Prop = AddressbookPropWrapper; type Prop = AddressbookPropWrapper;
type Error = Error; type Error = Error;
type Principal = User; type Principal = Principal;
const IS_COLLECTION: bool = true; fn is_collection(&self) -> bool {
true
}
fn get_resourcetype(&self) -> Resourcetype { fn get_resourcetype(&self) -> Resourcetype {
Resourcetype(&[ Resourcetype(&[
@@ -50,7 +52,7 @@ impl Resource for AddressbookResource {
fn get_prop( fn get_prop(
&self, &self,
puri: &impl PrincipalUri, puri: &impl PrincipalUri,
user: &User, user: &Principal,
prop: &AddressbookPropWrapperName, prop: &AddressbookPropWrapperName,
) -> Result<Self::Prop, Self::Error> { ) -> Result<Self::Prop, Self::Error> {
Ok(match prop { Ok(match prop {
@@ -136,7 +138,7 @@ impl Resource for AddressbookResource {
Some(&self.0.principal) Some(&self.0.principal)
} }
fn get_user_privileges(&self, user: &User) -> Result<UserPrivilegeSet, Self::Error> { fn get_user_privileges(&self, user: &Principal) -> Result<UserPrivilegeSet, Self::Error> {
Ok(UserPrivilegeSet::owner_only( Ok(UserPrivilegeSet::owner_only(
user.is_principal(&self.0.principal), user.is_principal(&self.0.principal),
)) ))

View File

@@ -3,6 +3,7 @@ use super::methods::report::route_report_addressbook;
use crate::address_object::AddressObjectResourceService; use crate::address_object::AddressObjectResourceService;
use crate::address_object::resource::AddressObjectResource; use crate::address_object::resource::AddressObjectResource;
use crate::addressbook::methods::get::route_get; use crate::addressbook::methods::get::route_get;
use crate::addressbook::methods::post::route_post;
use crate::addressbook::methods::put::route_put; use crate::addressbook::methods::put::route_put;
use crate::addressbook::resource::AddressbookResource; use crate::addressbook::resource::AddressbookResource;
use crate::{CardDavPrincipalUri, Error}; use crate::{CardDavPrincipalUri, Error};
@@ -13,7 +14,7 @@ use axum::handler::Handler;
use axum::response::Response; use axum::response::Response;
use futures_util::future::BoxFuture; use futures_util::future::BoxFuture;
use rustical_dav::resource::{AxumMethods, ResourceService}; use rustical_dav::resource::{AxumMethods, ResourceService};
use rustical_store::auth::User; use rustical_store::auth::Principal;
use rustical_store::{AddressbookStore, SubscriptionStore}; use rustical_store::{AddressbookStore, SubscriptionStore};
use std::convert::Infallible; use std::convert::Infallible;
use std::sync::Arc; use std::sync::Arc;
@@ -50,10 +51,10 @@ impl<AS: AddressbookStore, S: SubscriptionStore> ResourceService
type PathComponents = (String, String); // principal, addressbook_id type PathComponents = (String, String); // principal, addressbook_id
type Resource = AddressbookResource; type Resource = AddressbookResource;
type Error = Error; type Error = Error;
type Principal = User; type Principal = Principal;
type PrincipalUri = CardDavPrincipalUri; type PrincipalUri = CardDavPrincipalUri;
const DAV_HEADER: &str = "1, 3, access-control, addressbook"; const DAV_HEADER: &str = "1, 3, access-control, addressbook, webdav-push";
async fn get_resource( async fn get_resource(
&self, &self,
@@ -130,6 +131,13 @@ impl<AS: AddressbookStore, S: SubscriptionStore> AxumMethods for AddressbookReso
}) })
} }
fn post() -> Option<fn(Self, Request) -> BoxFuture<'static, Result<Response, Infallible>>> {
Some(|state, req| {
let mut service = Handler::with_state(route_post::<AS, S>, state);
Box::pin(Service::call(&mut service, req))
})
}
fn put() -> Option<fn(Self, Request) -> BoxFuture<'static, Result<Response, Infallible>>> { fn put() -> Option<fn(Self, Request) -> BoxFuture<'static, Result<Response, Infallible>>> {
Some(|state, req| { Some(|state, req| {
let mut service = Handler::with_state(route_put::<AS, S>, state); let mut service = Handler::with_state(route_put::<AS, S>, state);

View File

@@ -9,7 +9,7 @@ use rustical_dav::resources::RootResourceService;
use rustical_store::auth::middleware::AuthenticationLayer; use rustical_store::auth::middleware::AuthenticationLayer;
use rustical_store::{ use rustical_store::{
AddressbookStore, SubscriptionStore, AddressbookStore, SubscriptionStore,
auth::{AuthenticationProvider, User}, auth::{AuthenticationProvider, Principal},
}; };
use std::sync::Arc; use std::sync::Arc;
@@ -44,10 +44,12 @@ pub fn carddav_router<AP: AuthenticationProvider, A: AddressbookStore, S: Subscr
Router::new() Router::new()
.nest( .nest(
prefix, prefix,
RootResourceService::<_, User, CardDavPrincipalUri>::new(principal_service.clone()) RootResourceService::<_, Principal, CardDavPrincipalUri>::new(
.axum_router() principal_service.clone(),
.layer(AuthenticationLayer::new(auth_provider)) )
.layer(Extension(CardDavPrincipalUri(prefix))), .axum_router()
.layer(AuthenticationLayer::new(auth_provider))
.layer(Extension(CardDavPrincipalUri(prefix))),
) )
.route( .route(
"/.well-known/carddav", "/.well-known/carddav",

View File

@@ -5,7 +5,7 @@ use rustical_dav::resource::{PrincipalUri, Resource, ResourceName};
use rustical_dav::xml::{ use rustical_dav::xml::{
GroupMemberSet, GroupMembership, HrefElement, Resourcetype, ResourcetypeInner, GroupMemberSet, GroupMembership, HrefElement, Resourcetype, ResourcetypeInner,
}; };
use rustical_store::auth::User; use rustical_store::auth::Principal;
mod service; mod service;
pub use service::*; pub use service::*;
@@ -14,7 +14,7 @@ pub use prop::*;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct PrincipalResource { pub struct PrincipalResource {
principal: User, principal: Principal,
members: Vec<String>, members: Vec<String>,
} }
@@ -27,9 +27,11 @@ impl ResourceName for PrincipalResource {
impl Resource for PrincipalResource { impl Resource for PrincipalResource {
type Prop = PrincipalPropWrapper; type Prop = PrincipalPropWrapper;
type Error = Error; type Error = Error;
type Principal = User; type Principal = Principal;
const IS_COLLECTION: bool = true; fn is_collection(&self) -> bool {
true
}
fn get_resourcetype(&self) -> Resourcetype { fn get_resourcetype(&self) -> Resourcetype {
Resourcetype(&[ Resourcetype(&[
@@ -41,7 +43,7 @@ impl Resource for PrincipalResource {
fn get_prop( fn get_prop(
&self, &self,
puri: &impl PrincipalUri, puri: &impl PrincipalUri,
user: &User, user: &Principal,
prop: &PrincipalPropWrapperName, prop: &PrincipalPropWrapperName,
) -> Result<Self::Prop, Self::Error> { ) -> Result<Self::Prop, Self::Error> {
let principal_href = HrefElement::new(puri.principal_uri(&self.principal.id)); let principal_href = HrefElement::new(puri.principal_uri(&self.principal.id));
@@ -97,7 +99,7 @@ impl Resource for PrincipalResource {
Some(&self.principal.id) Some(&self.principal.id)
} }
fn get_user_privileges(&self, user: &User) -> Result<UserPrivilegeSet, Self::Error> { fn get_user_privileges(&self, user: &Principal) -> Result<UserPrivilegeSet, Self::Error> {
Ok(UserPrivilegeSet::owner_only( Ok(UserPrivilegeSet::owner_only(
user.is_principal(&self.principal.id), user.is_principal(&self.principal.id),
)) ))

View File

@@ -5,7 +5,7 @@ use crate::{CardDavPrincipalUri, Error};
use async_trait::async_trait; use async_trait::async_trait;
use axum::Router; use axum::Router;
use rustical_dav::resource::{AxumMethods, ResourceService}; use rustical_dav::resource::{AxumMethods, ResourceService};
use rustical_store::auth::{AuthenticationProvider, User}; use rustical_store::auth::{AuthenticationProvider, Principal};
use rustical_store::{AddressbookStore, SubscriptionStore}; use rustical_store::{AddressbookStore, SubscriptionStore};
use std::sync::Arc; use std::sync::Arc;
@@ -51,7 +51,7 @@ impl<A: AddressbookStore, AP: AuthenticationProvider, S: SubscriptionStore> Reso
type MemberType = AddressbookResource; type MemberType = AddressbookResource;
type Resource = PrincipalResource; type Resource = PrincipalResource;
type Error = Error; type Error = Error;
type Principal = User; type Principal = Principal;
type PrincipalUri = CardDavPrincipalUri; type PrincipalUri = CardDavPrincipalUri;
const DAV_HEADER: &str = "1, 3, access-control, addressbook"; const DAV_HEADER: &str = "1, 3, access-control, addressbook";

View File

@@ -26,3 +26,5 @@ tokio.workspace = true
http.workspace = true http.workspace = true
headers.workspace = true headers.workspace = true
strum.workspace = true strum.workspace = true
matchit.workspace = true
matchit-serde.workspace = true

View File

@@ -28,6 +28,9 @@ pub enum Error {
#[error("Precondition Failed")] #[error("Precondition Failed")]
PreconditionFailed, PreconditionFailed,
#[error("Forbidden")]
Forbidden,
} }
impl Error { impl Error {
@@ -49,6 +52,7 @@ impl Error {
Error::PropReadOnly => StatusCode::CONFLICT, Error::PropReadOnly => StatusCode::CONFLICT,
Error::PreconditionFailed => StatusCode::PRECONDITION_FAILED, Error::PreconditionFailed => StatusCode::PRECONDITION_FAILED,
Self::IOError(_) => StatusCode::INTERNAL_SERVER_ERROR, Self::IOError(_) => StatusCode::INTERNAL_SERVER_ERROR,
Self::Forbidden => StatusCode::FORBIDDEN,
} }
} }
} }

View File

@@ -1,25 +1,54 @@
use axum::{
extract::{Path, State},
response::{IntoResponse, Response},
};
use http::StatusCode;
use tracing::instrument;
use crate::{ use crate::{
header::{Depth, Overwrite}, header::{Depth, Overwrite},
resource::ResourceService, resource::ResourceService,
}; };
use axum::{
extract::{MatchedPath, Path, State},
response::{IntoResponse, Response},
};
use http::{HeaderMap, StatusCode, Uri};
use matchit_serde::ParamsDeserializer;
use serde::Deserialize;
use tracing::instrument;
#[instrument(skip(_path, _resource_service,))] #[instrument(skip(path, resource_service,))]
pub(crate) async fn axum_route_copy<R: ResourceService>( pub(crate) async fn axum_route_copy<R: ResourceService>(
Path(_path): Path<R::PathComponents>, Path(path): Path<R::PathComponents>,
State(_resource_service): State<R>, State(resource_service): State<R>,
depth: Option<Depth>, depth: Option<Depth>,
principal: R::Principal, principal: R::Principal,
overwrite: Overwrite, overwrite: Overwrite,
matched_path: MatchedPath,
header_map: HeaderMap,
) -> Result<Response, R::Error> { ) -> Result<Response, R::Error> {
// TODO: Actually implement, but to be WebDAV-compliant we must at least support this route but let destination = header_map
// can return a 403 error .get("Destination")
let _depth = depth.unwrap_or(Depth::Infinity); .ok_or(crate::Error::Forbidden)?
Ok(StatusCode::FORBIDDEN.into_response()) .to_str()
.map_err(|_| crate::Error::Forbidden)?;
let destination_uri: Uri = destination.parse().map_err(|_| crate::Error::Forbidden)?;
// TODO: Check that host also matches
let destination = destination_uri.path();
let mut router = matchit::Router::new();
router.insert(matched_path.as_str(), ()).unwrap();
if let Ok(matchit::Match { params, .. }) = router.at(destination) {
let params =
matchit_serde::Params::try_from(&params).map_err(|_| crate::Error::Forbidden)?;
let dest_path = R::PathComponents::deserialize(&ParamsDeserializer::new(params))
.map_err(|_| crate::Error::Forbidden)?;
if resource_service
.copy_resource(&path, &dest_path, &principal, overwrite.is_true())
.await?
{
// Overwritten
Ok(StatusCode::NO_CONTENT.into_response())
} else {
// Not overwritten
Ok(StatusCode::CREATED.into_response())
}
} else {
Ok(StatusCode::FORBIDDEN.into_response())
}
} }

View File

@@ -1,25 +1,54 @@
use axum::{
extract::{Path, State},
response::{IntoResponse, Response},
};
use http::StatusCode;
use tracing::instrument;
use crate::{ use crate::{
header::{Depth, Overwrite}, header::{Depth, Overwrite},
resource::ResourceService, resource::ResourceService,
}; };
use axum::{
extract::{MatchedPath, Path, State},
response::{IntoResponse, Response},
};
use http::{HeaderMap, StatusCode, Uri};
use matchit_serde::ParamsDeserializer;
use serde::Deserialize;
use tracing::instrument;
#[instrument(skip(_path, _resource_service,))] #[instrument(skip(path, resource_service,))]
pub(crate) async fn axum_route_move<R: ResourceService>( pub(crate) async fn axum_route_move<R: ResourceService>(
Path(_path): Path<R::PathComponents>, Path(path): Path<R::PathComponents>,
State(_resource_service): State<R>, State(resource_service): State<R>,
depth: Option<Depth>, depth: Option<Depth>,
principal: R::Principal, principal: R::Principal,
overwrite: Overwrite, overwrite: Overwrite,
matched_path: MatchedPath,
header_map: HeaderMap,
) -> Result<Response, R::Error> { ) -> Result<Response, R::Error> {
// TODO: Actually implement, but to be WebDAV-compliant we must at least support this route but let destination = header_map
// can return a 403 error .get("Destination")
let _depth = depth.unwrap_or(Depth::Infinity); .ok_or(crate::Error::Forbidden)?
Ok(StatusCode::FORBIDDEN.into_response()) .to_str()
.map_err(|_| crate::Error::Forbidden)?;
let destination_uri: Uri = destination.parse().map_err(|_| crate::Error::Forbidden)?;
// TODO: Check that host also matches
let destination = destination_uri.path();
let mut router = matchit::Router::new();
router.insert(matched_path.as_str(), ()).unwrap();
if let Ok(matchit::Match { params, .. }) = router.at(destination) {
let params =
matchit_serde::Params::try_from(&params).map_err(|_| crate::Error::Forbidden)?;
let dest_path = R::PathComponents::deserialize(&ParamsDeserializer::new(params))
.map_err(|_| crate::Error::Forbidden)?;
if resource_service
.copy_resource(&path, &dest_path, &principal, overwrite.is_true())
.await?
{
// Overwritten
Ok(StatusCode::NO_CONTENT.into_response())
} else {
// Not overwritten
Ok(StatusCode::CREATED.into_response())
}
} else {
Ok(StatusCode::FORBIDDEN.into_response())
}
} }

View File

@@ -18,7 +18,7 @@ mod methods;
mod principal_uri; mod principal_uri;
mod resource_service; mod resource_service;
pub use axum_methods::AxumMethods; pub use axum_methods::{AxumMethods, MethodFunction};
pub use axum_service::AxumService; pub use axum_service::AxumService;
pub use principal_uri::PrincipalUri; pub use principal_uri::PrincipalUri;
@@ -37,7 +37,7 @@ pub trait Resource: Clone + Send + 'static {
type Error: From<crate::Error>; type Error: From<crate::Error>;
type Principal: Principal; type Principal: Principal;
const IS_COLLECTION: bool; fn is_collection(&self) -> bool;
fn get_resourcetype(&self) -> Resourcetype; fn get_resourcetype(&self) -> Resourcetype;
@@ -111,7 +111,7 @@ pub trait Resource: Clone + Send + 'static {
) -> Result<ResponseElement<Self::Prop>, Self::Error> { ) -> Result<ResponseElement<Self::Prop>, Self::Error> {
// Collections have a trailing slash // Collections have a trailing slash
let mut path = path.to_string(); let mut path = path.to_string();
if Self::IS_COLLECTION && !path.ends_with('/') { if self.is_collection() && !path.ends_with('/') {
path.push('/'); path.push('/');
} }

View File

@@ -9,7 +9,13 @@ use serde::Deserialize;
#[async_trait] #[async_trait]
pub trait ResourceService: Clone + Sized + Send + Sync + AxumMethods + 'static { pub trait ResourceService: Clone + Sized + Send + Sync + AxumMethods + 'static {
type PathComponents: for<'de> Deserialize<'de> + Sized + Send + Sync + Clone + 'static; // defines how the resource URI maps to parameters, i.e. /{principal}/{calendar} -> (String, String) type PathComponents: std::fmt::Debug
+ for<'de> Deserialize<'de>
+ Sized
+ Send
+ Sync
+ Clone
+ 'static; // defines how the resource URI maps to parameters, i.e. /{principal}/{calendar} -> (String, String)
type MemberType: Resource<Error = Self::Error, Principal = Self::Principal> type MemberType: Resource<Error = Self::Error, Principal = Self::Principal>
+ super::ResourceName; + super::ResourceName;
type Resource: Resource<Error = Self::Error, Principal = Self::Principal>; type Resource: Resource<Error = Self::Error, Principal = Self::Principal>;
@@ -47,6 +53,28 @@ pub trait ResourceService: Clone + Sized + Send + Sync + AxumMethods + 'static {
Err(crate::Error::Unauthorized.into()) Err(crate::Error::Unauthorized.into())
} }
// Returns whether an existing resource was overwritten
async fn copy_resource(
&self,
_path: &Self::PathComponents,
_destination: &Self::PathComponents,
_user: &Self::Principal,
_overwrite: bool,
) -> Result<bool, Self::Error> {
Err(crate::Error::Forbidden.into())
}
// Returns whether an existing resource was overwritten
async fn move_resource(
&self,
_path: &Self::PathComponents,
_destination: &Self::PathComponents,
_user: &Self::Principal,
_overwrite: bool,
) -> Result<bool, Self::Error> {
Err(crate::Error::Forbidden.into())
}
fn axum_service(self) -> AxumService<Self> fn axum_service(self) -> AxumService<Self>
where where
Self: AxumMethods, Self: AxumMethods,

View File

@@ -24,7 +24,9 @@ impl<PR: Resource, P: Principal> Resource for RootResource<PR, P> {
type Error = PR::Error; type Error = PR::Error;
type Principal = P; type Principal = P;
const IS_COLLECTION: bool = true; fn is_collection(&self) -> bool {
true
}
fn get_resourcetype(&self) -> Resourcetype { fn get_resourcetype(&self) -> Resourcetype {
Resourcetype(&[ResourcetypeInner( Resourcetype(&[ResourcetypeInner(

View File

@@ -1,4 +1,5 @@
use crate::xml::TagList; use crate::xml::TagList;
use headers::{CacheControl, ContentType, HeaderMapExt};
use http::StatusCode; use http::StatusCode;
use quick_xml::name::Namespace; use quick_xml::name::Namespace;
use rustical_xml::{XmlRootTag, XmlSerialize, XmlSerializeRoot}; use rustical_xml::{XmlRootTag, XmlSerialize, XmlSerializeRoot};
@@ -109,7 +110,6 @@ impl<T1: XmlSerialize, T2: XmlSerialize> axum::response::IntoResponse
{ {
fn into_response(self) -> axum::response::Response { fn into_response(self) -> axum::response::Response {
use axum::body::Body; use axum::body::Body;
use http::header;
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);
@@ -118,9 +118,9 @@ impl<T1: XmlSerialize, T2: XmlSerialize> axum::response::IntoResponse
} }
let mut resp = axum::response::Response::builder().status(StatusCode::MULTI_STATUS); let mut resp = axum::response::Response::builder().status(StatusCode::MULTI_STATUS);
resp.headers_mut() let hdrs = resp.headers_mut().unwrap();
.unwrap() hdrs.typed_insert(ContentType::xml());
.insert(header::CONTENT_TYPE, "application/xml".try_into().unwrap()); hdrs.typed_insert(CacheControl::new().with_no_cache());
resp.body(Body::from(output)).unwrap() resp.body(Body::from(output)).unwrap()
} }
} }

View File

@@ -23,3 +23,8 @@ tokio.workspace = true
rustical_dav.workspace = true rustical_dav.workspace = true
rustical_store.workspace = true rustical_store.workspace = true
http.workspace = true http.workspace = true
base64.workspace = true
rand.workspace = true
ece.workspace = true
axum.workspace = true
openssl.workspace = true

View File

@@ -0,0 +1,23 @@
use axum::{
Router,
extract::{Path, State},
response::{IntoResponse, Response},
routing::delete,
};
use http::StatusCode;
use rustical_store::SubscriptionStore;
use std::sync::Arc;
async fn handle_delete<S: SubscriptionStore>(
State(store): State<Arc<S>>,
Path(id): Path<String>,
) -> Result<Response, rustical_store::Error> {
store.delete_subscription(&id).await?;
Ok((StatusCode::NO_CONTENT, "Unregistered").into_response())
}
pub fn subscription_service<S: SubscriptionStore>(sub_store: Arc<S>) -> Router {
Router::new()
.route("/push_subscription/{id}", delete(handle_delete::<S>))
.with_state(sub_store)
}

View File

@@ -1,14 +1,41 @@
mod extension; mod extension;
pub mod notifier;
mod prop; mod prop;
pub mod register; pub mod register;
use base64::Engine;
use derive_more::Constructor; use derive_more::Constructor;
pub use extension::*; pub use extension::*;
use http::{HeaderValue, Method, header};
pub use prop::*; pub use prop::*;
use rustical_store::{CollectionOperation, SubscriptionStore}; use reqwest::{Body, Url};
use std::sync::Arc; use rustical_store::{
CollectionOperation, CollectionOperationInfo, Subscription, SubscriptionStore,
};
use rustical_xml::{XmlRootTag, XmlSerialize, XmlSerializeRoot};
use std::{collections::HashMap, sync::Arc, time::Duration};
use tokio::sync::mpsc::Receiver; use tokio::sync::mpsc::Receiver;
use tracing::error; use tracing::{error, warn};
mod endpoints;
pub use endpoints::subscription_service;
#[derive(XmlSerialize, Debug)]
pub struct ContentUpdate {
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
sync_token: Option<String>,
}
#[derive(XmlSerialize, XmlRootTag, Debug)]
#[xml(root = b"push-message", ns = "rustical_dav::namespace::NS_DAVPUSH")]
#[xml(ns_prefix(
rustical_dav::namespace::NS_DAVPUSH = b"",
rustical_dav::namespace::NS_DAV = b"D",
))]
struct PushMessage {
#[xml(ns = "rustical_dav::namespace::NS_DAVPUSH")]
topic: String,
#[xml(ns = "rustical_dav::namespace::NS_DAVPUSH")]
content_update: Option<ContentUpdate>,
}
#[derive(Debug, Constructor)] #[derive(Debug, Constructor)]
pub struct DavPushController<S: SubscriptionStore> { pub struct DavPushController<S: SubscriptionStore> {
@@ -18,14 +45,176 @@ pub struct DavPushController<S: SubscriptionStore> {
impl<S: SubscriptionStore> DavPushController<S> { impl<S: SubscriptionStore> DavPushController<S> {
pub async fn notifier(&self, mut recv: Receiver<CollectionOperation>) { pub async fn notifier(&self, mut recv: Receiver<CollectionOperation>) {
while let Some(message) = recv.recv().await { loop {
let subscribers = match self.sub_store.get_subscriptions(&message.topic).await { // Make sure we don't flood the subscribers
Ok(subs) => subs, tokio::time::sleep(Duration::from_secs(10)).await;
Err(err) => { let mut messages = vec![];
error!("{err}"); recv.recv_many(&mut messages, 100).await;
continue;
// Right now we just have to show the latest content update by topic
// This might become more complicated in the future depending on what kind of updates
// we add
let mut latest_messages = HashMap::new();
for message in messages {
if matches!(message.data, CollectionOperationInfo::Content { .. }) {
latest_messages.insert(message.topic.to_string(), message);
} }
}; }
let messages = latest_messages.into_values();
for message in messages {
self.send_message(message).await;
}
}
}
async fn send_message(&self, message: CollectionOperation) {
let subscriptions = match self.sub_store.get_subscriptions(&message.topic).await {
Ok(subs) => subs,
Err(err) => {
error!("{err}");
return;
}
};
if subscriptions.is_empty() {
return;
}
if matches!(message.data, CollectionOperationInfo::Delete) {
// Collection has been deleted, but we cannot handle that
return;
}
let content_update = if let CollectionOperationInfo::Content { sync_token } = message.data {
Some(ContentUpdate {
sync_token: Some(sync_token),
})
} else {
None
};
let push_message = PushMessage {
topic: message.topic,
content_update,
};
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);
if let Err(err) = push_message.serialize_root(&mut writer) {
error!("Could not serialize push message: {}", err);
return;
}
let payload = String::from_utf8(output).unwrap();
for subsciption in subscriptions {
if let Some(allowed_push_servers) = &self.allowed_push_servers {
if let Ok(url) = Url::parse(&subsciption.push_resource) {
let origin = url.origin().unicode_serialization();
if !allowed_push_servers.contains(&origin) {
warn!(
"Deleting subscription {} on topic {} because the endpoint is not in the list of allowed push servers",
subsciption.id, subsciption.topic
);
self.try_delete_subscription(&subsciption.id).await;
}
} else {
warn!(
"Deleting subscription {} on topic {} because of invalid URL",
subsciption.id, subsciption.topic
);
self.try_delete_subscription(&subsciption.id).await;
};
}
if let Err(err) = self.send_payload(&payload, &subsciption).await {
error!("An error occured sending out a push notification: {err}");
if err.is_permament_error() {
warn!(
"Deleting subscription {} on topic {}",
subsciption.id, subsciption.topic
);
self.try_delete_subscription(&subsciption.id).await;
}
}
}
}
async fn try_delete_subscription(&self, sub_id: &str) {
if let Err(err) = self.sub_store.delete_subscription(sub_id).await {
error!("Error deleting subsciption: {err}");
}
}
async fn send_payload(
&self,
payload: &str,
subsciption: &Subscription,
) -> Result<(), NotifierError> {
if subsciption.public_key_type != "p256dh" {
return Err(NotifierError::InvalidPublicKeyType(
subsciption.public_key_type.to_string(),
));
}
let endpoint = subsciption.push_resource.parse().map_err(|_| {
NotifierError::InvalidEndpointUrl(subsciption.push_resource.to_string())
})?;
let ua_public = base64::engine::general_purpose::URL_SAFE_NO_PAD
.decode(&subsciption.public_key)
.map_err(|_| NotifierError::InvalidKeyEncoding)?;
let auth_secret = base64::engine::general_purpose::URL_SAFE_NO_PAD
.decode(&subsciption.auth_secret)
.map_err(|_| NotifierError::InvalidKeyEncoding)?;
let client = reqwest::ClientBuilder::new()
.build()
.map_err(NotifierError::from)?;
let payload = ece::encrypt(&ua_public, &auth_secret, payload.as_bytes())?;
let mut request = reqwest::Request::new(Method::POST, endpoint);
*request.body_mut() = Some(Body::from(payload));
let hdrs = request.headers_mut();
hdrs.insert(
header::CONTENT_ENCODING,
HeaderValue::from_static("aes128gcm"),
);
hdrs.insert(
header::CONTENT_TYPE,
HeaderValue::from_static("application/octet-stream"),
);
client.execute(request).await?;
Ok(())
}
}
#[derive(Debug, thiserror::Error)]
enum NotifierError {
#[error("Invalid public key type: {0}")]
InvalidPublicKeyType(String),
#[error("Invalid endpoint URL: {0}")]
InvalidEndpointUrl(String),
#[error("Invalid key encoding")]
InvalidKeyEncoding,
#[error(transparent)]
EceError(#[from] ece::Error),
#[error(transparent)]
ReqwestError(#[from] reqwest::Error),
}
impl NotifierError {
// Decide whether the error should cause the subscription to be removed
pub fn is_permament_error(&self) -> bool {
match self {
Self::InvalidPublicKeyType(_)
| Self::InvalidEndpointUrl(_)
| Self::InvalidKeyEncoding => true,
Self::EceError(err) => matches!(
err,
ece::Error::InvalidAuthSecret | ece::Error::InvalidKeyLength
),
Self::ReqwestError(_) => false,
} }
} }
} }

View File

@@ -1,147 +0,0 @@
use 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::{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 {
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
topic: String,
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
sync_token: Option<String>,
}
#[derive(XmlSerialize, XmlRootTag, Debug)]
#[xml(root = b"push-message", ns = "rustical_dav::namespace::NS_DAVPUSH")]
#[xml(ns_prefix(
rustical_dav::namespace::NS_DAVPUSH = b"",
rustical_dav::namespace::NS_DAV = b"D",
))]
struct PushMessage {
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
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>,
sub_store: Arc<impl SubscriptionStore>,
) {
let client = reqwest::Client::new();
while let Some(message) = recv.recv().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 {
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"<?xml version=\"1.0\" encoding=\"utf-8\"?>\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 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();
// 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.execute(request).await {
// 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

@@ -5,7 +5,7 @@
}, },
"compilerOptions": { "compilerOptions": {
"lib": [ "lib": [
"ES2020", "ES2024",
"DOM", "DOM",
"DOM.Iterable" "DOM.Iterable"
] ]

View File

@@ -1,5 +1,6 @@
import { html, LitElement } from "lit"; import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators.js"; import { customElement, property } from "lit/decorators.js";
import { Ref, createRef, ref } from 'lit/directives/ref.js';
import { createClient } from "webdav"; import { createClient } from "webdav";
@customElement("create-addressbook-form") @customElement("create-addressbook-form")
@@ -24,12 +25,15 @@ export class CreateAddressbookForm extends LitElement {
@property() @property()
description: String = '' description: String = ''
dialog: Ref<HTMLDialogElement> = createRef()
form: Ref<HTMLFormElement> = createRef()
override render() { override render() {
return html` return html`
<section> <button @click=${() => this.dialog.value.showModal()}>Create addressbook</button>
<h3>Create calendar</h3> <dialog ${ref(this.dialog)}>
<form @submit=${this.submit}> <h3>Create addressbook</h3>
<form @submit=${this.submit} ${ref(this.form)}>
<label> <label>
id id
<input type="text" name="id" @change=${e => this.id = e.target.value} /> <input type="text" name="id" @change=${e => this.id = e.target.value} />
@@ -46,8 +50,9 @@ export class CreateAddressbookForm extends LitElement {
</label> </label>
<br> <br>
<button type="submit">Create</button> <button type="submit">Create</button>
<button type="submit" @click=${event => { event.preventDefault(); this.dialog.value.close(); this.form.value.reset() }}> Cancel </button>
</form> </form>
</section> </dialog>
` `
} }

View File

@@ -1,5 +1,6 @@
import { html, LitElement } from "lit"; import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators.js"; import { customElement, property } from "lit/decorators.js";
import { Ref, createRef, ref } from 'lit/directives/ref.js';
import { createClient } from "webdav"; import { createClient } from "webdav";
@customElement("create-calendar-form") @customElement("create-calendar-form")
@@ -30,12 +31,16 @@ export class CreateCalendarForm extends LitElement {
@property() @property()
components: Set<"VEVENT" | "VTODO" | "VJOURNAL"> = new Set() components: Set<"VEVENT" | "VTODO" | "VJOURNAL"> = new Set()
dialog: Ref<HTMLDialogElement> = createRef()
form: Ref<HTMLFormElement> = createRef()
override render() { override render() {
return html` return html`
<section> <button @click=${() => this.dialog.value.showModal()}>Create calendar</button>
<dialog ${ref(this.dialog)}>
<h3>Create calendar</h3> <h3>Create calendar</h3>
<form @submit=${this.submit}> <form @submit=${this.submit} ${ref(this.form)}>
<label> <label>
id id
<input type="text" name="id" @change=${e => this.id = e.target.value} /> <input type="text" name="id" @change=${e => this.id = e.target.value} />
@@ -69,9 +74,10 @@ export class CreateCalendarForm extends LitElement {
`)} `)}
<br> <br>
<button type="submit">Create</button> <button type="submit">Create</button>
</form> <button type="submit" @click=${event => { event.preventDefault(); this.dialog.value.close(); this.form.value.reset() }}> Cancel </button>
</section> </form>
` </dialog>
`
} }
async submit(e: SubmitEvent) { async submit(e: SubmitEvent) {
@@ -89,7 +95,7 @@ export class CreateCalendarForm extends LitElement {
alert("No calendar components selected") alert("No calendar components selected")
return return
} }
await this.client.createDirectory(`/principal/${this.user}/${this.id}`, { await this.client.createDirectory(`/ principal / ${this.user}/${this.id}`, {
data: ` data: `
<mkcol xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CS="http://calendarserver.org/ns/" xmlns:ICAL="http://apple.com/ns/ical/"> <mkcol xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CS="http://calendarserver.org/ns/" xmlns:ICAL="http://apple.com/ns/ical/">
<set> <set>

View File

@@ -1,10 +1,13 @@
{ {
"module": "nodenext", "module": "nodenext",
"moduleResolution": "nodenext",
"compilerOptions": { "compilerOptions": {
"target": "es2020", "target": "es2024",
"experimentalDecorators": true, "experimentalDecorators": true,
"useDefineForClassFields": false "useDefineForClassFields": false,
"lib": [
"dom",
"es2024"
]
}, },
"include": [ "include": [
"lib/**/*.ts" "lib/**/*.ts"

View File

@@ -1,45 +1,49 @@
import { i as d, x as m } from "./lit-Dq9MfRDi.mjs"; import { i as c, x as u } from "./lit-CWlWuEHk.mjs";
import { n, t as c } from "./property-DwhV4xIV.mjs"; import { e as d, n as m, a as o, t as h } from "./ref-DuYNkSJ_.mjs";
import { a as u } from "./webdav-Bz4I5vNH.mjs"; import { a as b } from "./webdav-Bz4I5vNH.mjs";
var h = Object.defineProperty, y = Object.getOwnPropertyDescriptor, r = (e, a, o, s) => { var y = Object.defineProperty, f = Object.getOwnPropertyDescriptor, a = (t, s, l, r) => {
for (var t = s > 1 ? void 0 : s ? y(a, o) : a, p = e.length - 1, l; p >= 0; p--) for (var e = r > 1 ? void 0 : r ? f(s, l) : s, n = t.length - 1, p; n >= 0; n--)
(l = e[p]) && (t = (s ? l(a, o, t) : l(t)) || t); (p = t[n]) && (e = (r ? p(s, l, e) : p(e)) || e);
return s && t && h(a, o, t), t; return r && e && y(s, l, e), e;
}; };
let i = class extends d { let i = class extends c {
constructor() { constructor() {
super(), this.client = u("/carddav"), this.user = "", this.id = "", this.displayname = "", this.description = ""; super(), this.client = b("/carddav"), this.user = "", this.id = "", this.displayname = "", this.description = "", this.dialog = d(), this.form = d();
} }
createRenderRoot() { createRenderRoot() {
return this; return this;
} }
render() { render() {
return m` return u`
<section> <button @click=${() => this.dialog.value.showModal()}>Create addressbook</button>
<h3>Create calendar</h3> <dialog ${m(this.dialog)}>
<form @submit=${this.submit}> <h3>Create addressbook</h3>
<form @submit=${this.submit} ${m(this.form)}>
<label> <label>
id id
<input type="text" name="id" @change=${(e) => this.id = e.target.value} /> <input type="text" name="id" @change=${(t) => this.id = t.target.value} />
</label> </label>
<br> <br>
<label> <label>
Displayname Displayname
<input type="text" name="displayname" value=${this.displayname} @change=${(e) => this.displayname = e.target.value} /> <input type="text" name="displayname" value=${this.displayname} @change=${(t) => this.displayname = t.target.value} />
</label> </label>
<br> <br>
<label> <label>
Description Description
<input type="text" name="description" @change=${(e) => this.description = e.target.value} /> <input type="text" name="description" @change=${(t) => this.description = t.target.value} />
</label> </label>
<br> <br>
<button type="submit">Create</button> <button type="submit">Create</button>
<button type="submit" @click=${(t) => {
t.preventDefault(), this.dialog.value.close(), this.form.value.reset();
}}> Cancel </button>
</form> </form>
</section> </dialog>
`; `;
} }
async submit(e) { async submit(t) {
if (console.log(this.displayname), e.preventDefault(), !this.id) { if (console.log(this.displayname), t.preventDefault(), !this.id) {
alert("Empty id"); alert("Empty id");
return; return;
} }
@@ -61,20 +65,20 @@ let i = class extends d {
}), window.location.reload(), null; }), window.location.reload(), null;
} }
}; };
r([ a([
n() o()
], i.prototype, "user", 2); ], i.prototype, "user", 2);
r([ a([
n() o()
], i.prototype, "id", 2); ], i.prototype, "id", 2);
r([ a([
n() o()
], i.prototype, "displayname", 2); ], i.prototype, "displayname", 2);
r([ a([
n() o()
], i.prototype, "description", 2); ], i.prototype, "description", 2);
i = r([ i = a([
c("create-addressbook-form") h("create-addressbook-form")
], i); ], i);
export { export {
i as CreateAddressbookForm i as CreateAddressbookForm

View File

@@ -1,62 +1,66 @@
import { i as m, x as c } from "./lit-Dq9MfRDi.mjs"; import { i as u, x as c } from "./lit-CWlWuEHk.mjs";
import { n as s, t as d } from "./property-DwhV4xIV.mjs"; import { e as d, n as m, a as o, t as h } from "./ref-DuYNkSJ_.mjs";
import { a as u } from "./webdav-Bz4I5vNH.mjs"; import { a as b } from "./webdav-Bz4I5vNH.mjs";
var h = Object.defineProperty, b = Object.getOwnPropertyDescriptor, a = (e, t, o, n) => { var y = Object.defineProperty, $ = Object.getOwnPropertyDescriptor, a = (t, e, l, s) => {
for (var i = n > 1 ? void 0 : n ? b(t, o) : t, l = e.length - 1, p; l >= 0; l--) for (var i = s > 1 ? void 0 : s ? $(e, l) : e, n = t.length - 1, p; n >= 0; n--)
(p = e[l]) && (i = (n ? p(t, o, i) : p(i)) || i); (p = t[n]) && (i = (s ? p(e, l, i) : p(i)) || i);
return n && i && h(t, o, i), i; return s && i && y(e, l, i), i;
}; };
let r = class extends m { let r = class extends u {
constructor() { constructor() {
super(), this.client = u("/caldav"), this.user = "", this.id = "", this.displayname = "", this.description = "", this.color = "", this.subscriptionUrl = "", this.components = /* @__PURE__ */ new Set(); super(), this.client = b("/caldav"), this.user = "", this.id = "", this.displayname = "", this.description = "", this.color = "", this.subscriptionUrl = "", this.components = /* @__PURE__ */ new Set(), this.dialog = d(), this.form = d();
} }
createRenderRoot() { createRenderRoot() {
return this; return this;
} }
render() { render() {
return c` return c`
<section> <button @click=${() => this.dialog.value.showModal()}>Create calendar</button>
<dialog ${m(this.dialog)}>
<h3>Create calendar</h3> <h3>Create calendar</h3>
<form @submit=${this.submit}> <form @submit=${this.submit} ${m(this.form)}>
<label> <label>
id id
<input type="text" name="id" @change=${(e) => this.id = e.target.value} /> <input type="text" name="id" @change=${(t) => this.id = t.target.value} />
</label> </label>
<br> <br>
<label> <label>
Displayname Displayname
<input type="text" name="displayname" value=${this.displayname} @change=${(e) => this.displayname = e.target.value} /> <input type="text" name="displayname" value=${this.displayname} @change=${(t) => this.displayname = t.target.value} />
</label> </label>
<br> <br>
<label> <label>
Description Description
<input type="text" name="description" @change=${(e) => this.description = e.target.value} /> <input type="text" name="description" @change=${(t) => this.description = t.target.value} />
</label> </label>
<br> <br>
<label> <label>
Color Color
<input type="color" name="color" @change=${(e) => this.color = e.target.value} /> <input type="color" name="color" @change=${(t) => this.color = t.target.value} />
</label> </label>
<br> <br>
<label> <label>
Subscription URL Subscription URL
<input type="text" name="subscription_url" @change=${(e) => this.subscriptionUrl = e.target.value} /> <input type="text" name="subscription_url" @change=${(t) => this.subscriptionUrl = t.target.value} />
</label> </label>
<br> <br>
${["VEVENT", "VTODO", "VJOURNAL"].map((e) => c` ${["VEVENT", "VTODO", "VJOURNAL"].map((t) => c`
<label> <label>
Support ${e} Support ${t}
<input type="checkbox" value=${e} @change=${(t) => t.target.checked ? this.components.add(t.target.value) : this.components.delete(t.target.value)} /> <input type="checkbox" value=${t} @change=${(e) => e.target.checked ? this.components.add(e.target.value) : this.components.delete(e.target.value)} />
</label> </label>
`)} `)}
<br> <br>
<button type="submit">Create</button> <button type="submit">Create</button>
</form> <button type="submit" @click=${(t) => {
</section> t.preventDefault(), this.dialog.value.close(), this.form.value.reset();
`; }}> Cancel </button>
</form>
</dialog>
`;
} }
async submit(e) { async submit(t) {
if (console.log(this.displayname), e.preventDefault(), !this.id) { if (console.log(this.displayname), t.preventDefault(), !this.id) {
alert("Empty id"); alert("Empty id");
return; return;
} }
@@ -68,7 +72,7 @@ let r = class extends m {
alert("No calendar components selected"); alert("No calendar components selected");
return; return;
} }
return await this.client.createDirectory(`/principal/${this.user}/${this.id}`, { return await this.client.createDirectory(`/ principal / ${this.user}/${this.id}`, {
data: ` data: `
<mkcol xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CS="http://calendarserver.org/ns/" xmlns:ICAL="http://apple.com/ns/ical/"> <mkcol xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CS="http://calendarserver.org/ns/" xmlns:ICAL="http://apple.com/ns/ical/">
<set> <set>
@@ -78,7 +82,7 @@ let r = class extends m {
${this.color ? `<ICAL:calendar-color>${this.color}</ICAL:calendar-color>` : ""} ${this.color ? `<ICAL:calendar-color>${this.color}</ICAL:calendar-color>` : ""}
${this.subscriptionUrl ? `<CS:source>${this.subscriptionUrl}</CS:source>` : ""} ${this.subscriptionUrl ? `<CS:source>${this.subscriptionUrl}</CS:source>` : ""}
<CAL:supported-calendar-component-set> <CAL:supported-calendar-component-set>
${Array.from(this.components.keys()).map((t) => `<CAL:comp name="${t}" />`).join(` ${Array.from(this.components.keys()).map((e) => `<CAL:comp name="${e}" />`).join(`
`)} `)}
</CAL:supported-calendar-component-set> </CAL:supported-calendar-component-set>
</prop> </prop>
@@ -89,28 +93,28 @@ let r = class extends m {
} }
}; };
a([ a([
s() o()
], r.prototype, "user", 2); ], r.prototype, "user", 2);
a([ a([
s() o()
], r.prototype, "id", 2); ], r.prototype, "id", 2);
a([ a([
s() o()
], r.prototype, "displayname", 2); ], r.prototype, "displayname", 2);
a([ a([
s() o()
], r.prototype, "description", 2); ], r.prototype, "description", 2);
a([ a([
s() o()
], r.prototype, "color", 2); ], r.prototype, "color", 2);
a([ a([
s() o()
], r.prototype, "subscriptionUrl", 2); ], r.prototype, "subscriptionUrl", 2);
a([ a([
s() o()
], r.prototype, "components", 2); ], r.prototype, "components", 2);
r = a([ r = a([
d("create-calendar-form") h("create-calendar-form")
], r); ], r);
export { export {
r as CreateCalendarForm r as CreateCalendarForm

View File

@@ -543,6 +543,7 @@ const z = y.litElementPolyfillSupport;
z == null || z({ LitElement: T }); z == null || z({ LitElement: T });
(y.litElementVersions ?? (y.litElementVersions = [])).push("4.2.0"); (y.litElementVersions ?? (y.litElementVersions = [])).push("4.2.0");
export { export {
d as E,
et as f, et as f,
T as i, T as i,
j as u, j as u,

View File

@@ -1,47 +0,0 @@
import { f as d, u as l } from "./lit-Dq9MfRDi.mjs";
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const f = (t) => (r, e) => {
e !== void 0 ? e.addInitializer(() => {
customElements.define(t, r);
}) : customElements.define(t, r);
};
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const p = { attribute: !0, type: String, converter: l, reflect: !1, hasChanged: d }, u = (t = p, r, e) => {
const { kind: i, metadata: a } = e;
let n = globalThis.litPropertyMetadata.get(a);
if (n === void 0 && globalThis.litPropertyMetadata.set(a, n = /* @__PURE__ */ new Map()), i === "setter" && ((t = Object.create(t)).wrapped = !0), n.set(e.name, t), i === "accessor") {
const { name: o } = e;
return { set(s) {
const c = r.get.call(this);
r.set.call(this, s), this.requestUpdate(o, c, t);
}, init(s) {
return s !== void 0 && this.C(o, void 0, t, s), s;
} };
}
if (i === "setter") {
const { name: o } = e;
return function(s) {
const c = this[o];
r.call(this, s), this.requestUpdate(o, c, t);
};
}
throw Error("Unsupported decorator location: " + i);
};
function m(t) {
return (r, e) => typeof e == "object" ? u(t, r, e) : ((i, a, n) => {
const o = a.hasOwnProperty(n);
return a.constructor.createProperty(n, i), o ? Object.getOwnPropertyDescriptor(a, n) : void 0;
})(t, r, e);
}
export {
m as n,
f as t
};

View File

@@ -0,0 +1,172 @@
import { f, u as _, E as $ } from "./lit-CWlWuEHk.mjs";
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const T = (t) => (e, s) => {
s !== void 0 ? s.addInitializer(() => {
customElements.define(t, e);
}) : customElements.define(t, e);
};
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const A = { attribute: !0, type: String, converter: _, reflect: !1, hasChanged: f }, p = (t = A, e, s) => {
const { kind: i, metadata: n } = s;
let r = globalThis.litPropertyMetadata.get(n);
if (r === void 0 && globalThis.litPropertyMetadata.set(n, r = /* @__PURE__ */ new Map()), i === "setter" && ((t = Object.create(t)).wrapped = !0), r.set(s.name, t), i === "accessor") {
const { name: o } = s;
return { set(h) {
const l = e.get.call(this);
e.set.call(this, h), this.requestUpdate(o, l, t);
}, init(h) {
return h !== void 0 && this.C(o, void 0, t, h), h;
} };
}
if (i === "setter") {
const { name: o } = s;
return function(h) {
const l = this[o];
e.call(this, h), this.requestUpdate(o, l, t);
};
}
throw Error("Unsupported decorator location: " + i);
};
function O(t) {
return (e, s) => typeof s == "object" ? p(t, e, s) : ((i, n, r) => {
const o = n.hasOwnProperty(r);
return n.constructor.createProperty(r, i), o ? Object.getOwnPropertyDescriptor(n, r) : void 0;
})(t, e, s);
}
/**
* @license
* Copyright 2020 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const v = (t) => t.strings === void 0;
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const g = { CHILD: 2 }, C = (t) => (...e) => ({ _$litDirective$: t, values: e });
class m {
constructor(e) {
}
get _$AU() {
return this._$AM._$AU;
}
_$AT(e, s, i) {
this._$Ct = e, this._$AM = s, this._$Ci = i;
}
_$AS(e, s) {
return this.update(e, s);
}
update(e, s) {
return this.render(...s);
}
}
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const c = (t, e) => {
var i;
const s = t._$AN;
if (s === void 0) return !1;
for (const n of s) (i = n._$AO) == null || i.call(n, e, !1), c(n, e);
return !0;
}, a = (t) => {
let e, s;
do {
if ((e = t._$AM) === void 0) break;
s = e._$AN, s.delete(t), t = e;
} while ((s == null ? void 0 : s.size) === 0);
}, u = (t) => {
for (let e; e = t._$AM; t = e) {
let s = e._$AN;
if (s === void 0) e._$AN = s = /* @__PURE__ */ new Set();
else if (s.has(t)) break;
s.add(t), M(e);
}
};
function y(t) {
this._$AN !== void 0 ? (a(this), this._$AM = t, u(this)) : this._$AM = t;
}
function G(t, e = !1, s = 0) {
const i = this._$AH, n = this._$AN;
if (n !== void 0 && n.size !== 0) if (e) if (Array.isArray(i)) for (let r = s; r < i.length; r++) c(i[r], !1), a(i[r]);
else i != null && (c(i, !1), a(i));
else c(this, t);
}
const M = (t) => {
t.type == g.CHILD && (t._$AP ?? (t._$AP = G), t._$AQ ?? (t._$AQ = y));
};
class b extends m {
constructor() {
super(...arguments), this._$AN = void 0;
}
_$AT(e, s, i) {
super._$AT(e, s, i), u(this), this.isConnected = e._$AU;
}
_$AO(e, s = !0) {
var i, n;
e !== this.isConnected && (this.isConnected = e, e ? (i = this.reconnected) == null || i.call(this) : (n = this.disconnected) == null || n.call(this)), s && (c(this, e), a(this));
}
setValue(e) {
if (v(this._$Ct)) this._$Ct._$AI(e, this);
else {
const s = [...this._$Ct._$AH];
s[this._$Ci] = e, this._$Ct._$AI(s, this, 0);
}
}
disconnected() {
}
reconnected() {
}
}
/**
* @license
* Copyright 2020 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const P = () => new w();
class w {
}
const d = /* @__PURE__ */ new WeakMap(), U = C(class extends b {
render(t) {
return $;
}
update(t, [e]) {
var i;
const s = e !== this.G;
return s && this.G !== void 0 && this.rt(void 0), (s || this.lt !== this.ct) && (this.G = e, this.ht = (i = t.options) == null ? void 0 : i.host, this.rt(this.ct = t.element)), $;
}
rt(t) {
if (this.isConnected || (t = void 0), typeof this.G == "function") {
const e = this.ht ?? globalThis;
let s = d.get(e);
s === void 0 && (s = /* @__PURE__ */ new WeakMap(), d.set(e, s)), s.get(this.G) !== void 0 && this.G.call(this.ht, void 0), s.set(this.G, t), t !== void 0 && this.G.call(this.ht, t);
} else this.G.value = t;
}
get lt() {
var t, e;
return typeof this.G == "function" ? (t = d.get(this.ht ?? globalThis)) == null ? void 0 : t.get(this.G) : (e = this.G) == null ? void 0 : e.value;
}
disconnected() {
this.lt === this.ct && this.rt(void 0);
}
reconnected() {
this.rt(this.ct);
}
});
export {
O as a,
P as e,
U as n,
T as t
};

File diff suppressed because it is too large Load Diff

View File

@@ -13,7 +13,7 @@ use axum_extra::{TypedHeader, extract::Host};
use chrono::{Duration, Utc}; use chrono::{Duration, Utc};
use headers::UserAgent; use headers::UserAgent;
use http::StatusCode; use http::StatusCode;
use rustical_store::auth::{AuthenticationProvider, User}; use rustical_store::auth::{AuthenticationProvider, Principal};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::sync::Arc; use std::sync::Arc;
use tracing::instrument; use tracing::instrument;
@@ -101,7 +101,7 @@ struct NextcloudLoginPage {
pub(crate) async fn get_nextcloud_flow( pub(crate) async fn get_nextcloud_flow(
Extension(state): Extension<Arc<NextcloudFlows>>, Extension(state): Extension<Arc<NextcloudFlows>>,
Path(flow_id): Path<String>, Path(flow_id): Path<String>,
user: User, user: Principal,
) -> Result<Response, rustical_store::Error> { ) -> Result<Response, rustical_store::Error> {
if let Some(flow) = state.flows.read().await.get(&flow_id) { if let Some(flow) = state.flows.read().await.get(&flow_id) {
Ok(Html( Ok(Html(
@@ -131,7 +131,7 @@ struct NextcloudLoginSuccessPage {
#[instrument(skip(state))] #[instrument(skip(state))]
pub(crate) async fn post_nextcloud_flow( pub(crate) async fn post_nextcloud_flow(
user: User, user: Principal,
Extension(state): Extension<Arc<NextcloudFlows>>, Extension(state): Extension<Arc<NextcloudFlows>>,
Path(flow_id): Path<String>, Path(flow_id): Path<String>,
Host(host): Host, Host(host): Host,

View File

@@ -2,7 +2,7 @@ use std::sync::Arc;
use async_trait::async_trait; use async_trait::async_trait;
use rustical_oidc::UserStore; use rustical_oidc::UserStore;
use rustical_store::auth::{AuthenticationProvider, User}; use rustical_store::auth::{AuthenticationProvider, Principal};
pub struct OidcUserStore<AP: AuthenticationProvider>(pub Arc<AP>); pub struct OidcUserStore<AP: AuthenticationProvider>(pub Arc<AP>);
@@ -23,7 +23,7 @@ impl<AP: AuthenticationProvider> UserStore for OidcUserStore<AP> {
async fn insert_user(&self, id: &str) -> Result<(), Self::Error> { async fn insert_user(&self, id: &str) -> Result<(), Self::Error> {
self.0 self.0
.insert_principal( .insert_principal(
User { Principal {
id: id.to_owned(), id: id.to_owned(),
displayname: None, displayname: None,
principal_type: Default::default(), principal_type: Default::default(),

View File

@@ -10,7 +10,7 @@ use axum::{
use axum_extra::TypedHeader; use axum_extra::TypedHeader;
use headers::Referer; use headers::Referer;
use http::StatusCode; use http::StatusCode;
use rustical_store::{Addressbook, AddressbookStore, auth::User}; use rustical_store::{Addressbook, AddressbookStore, auth::Principal};
#[derive(Template, WebTemplate)] #[derive(Template, WebTemplate)]
#[template(path = "pages/addressbook.html")] #[template(path = "pages/addressbook.html")]
@@ -21,7 +21,7 @@ struct AddressbookPage {
pub async fn route_addressbook<AS: AddressbookStore>( pub async fn route_addressbook<AS: AddressbookStore>(
Path((owner, addrbook_id)): Path<(String, String)>, Path((owner, addrbook_id)): Path<(String, String)>,
Extension(store): Extension<Arc<AS>>, Extension(store): Extension<Arc<AS>>,
user: User, user: Principal,
) -> Result<Response, rustical_store::Error> { ) -> Result<Response, rustical_store::Error> {
if !user.is_principal(&owner) { if !user.is_principal(&owner) {
return Ok(StatusCode::UNAUTHORIZED.into_response()); return Ok(StatusCode::UNAUTHORIZED.into_response());
@@ -35,7 +35,7 @@ pub async fn route_addressbook<AS: AddressbookStore>(
pub async fn route_addressbook_restore<AS: AddressbookStore>( pub async fn route_addressbook_restore<AS: AddressbookStore>(
Path((owner, addressbook_id)): Path<(String, String)>, Path((owner, addressbook_id)): Path<(String, String)>,
Extension(store): Extension<Arc<AS>>, Extension(store): Extension<Arc<AS>>,
user: User, user: Principal,
referer: Option<TypedHeader<Referer>>, referer: Option<TypedHeader<Referer>>,
) -> Result<Response, rustical_store::Error> { ) -> Result<Response, rustical_store::Error> {
if !user.is_principal(&owner) { if !user.is_principal(&owner) {
@@ -51,7 +51,7 @@ pub async fn route_addressbook_restore<AS: AddressbookStore>(
pub async fn route_delete_addressbook<AS: AddressbookStore>( pub async fn route_delete_addressbook<AS: AddressbookStore>(
Path((owner, addressbook_id)): Path<(String, String)>, Path((owner, addressbook_id)): Path<(String, String)>,
Extension(store): Extension<Arc<AS>>, Extension(store): Extension<Arc<AS>>,
user: User, user: Principal,
) -> Result<Response, rustical_store::Error> { ) -> Result<Response, rustical_store::Error> {
if !user.is_principal(&owner) { if !user.is_principal(&owner) {
return Ok(StatusCode::UNAUTHORIZED.into_response()); return Ok(StatusCode::UNAUTHORIZED.into_response());

View File

@@ -12,7 +12,7 @@ use headers::{ContentType, HeaderMapExt};
use http::{HeaderValue, StatusCode, header}; use http::{HeaderValue, StatusCode, header};
use percent_encoding::{CONTROLS, utf8_percent_encode}; use percent_encoding::{CONTROLS, utf8_percent_encode};
use rand::{Rng, distr::Alphanumeric}; use rand::{Rng, distr::Alphanumeric};
use rustical_store::auth::{AuthenticationProvider, User}; use rustical_store::auth::{AuthenticationProvider, Principal};
use serde::Deserialize; use serde::Deserialize;
use uuid::Uuid; use uuid::Uuid;
@@ -47,7 +47,7 @@ pub(crate) struct PostAppTokenForm {
} }
pub async fn route_post_app_token<AP: AuthenticationProvider>( pub async fn route_post_app_token<AP: AuthenticationProvider>(
user: User, user: Principal,
Extension(auth_provider): Extension<Arc<AP>>, Extension(auth_provider): Extension<Arc<AP>>,
Path(user_id): Path<String>, Path(user_id): Path<String>,
Host(hostname): Host, Host(hostname): Host,
@@ -96,7 +96,7 @@ pub async fn route_post_app_token<AP: AuthenticationProvider>(
} }
pub async fn route_delete_app_token<AP: AuthenticationProvider>( pub async fn route_delete_app_token<AP: AuthenticationProvider>(
user: User, user: Principal,
Extension(auth_provider): Extension<Arc<AP>>, Extension(auth_provider): Extension<Arc<AP>>,
Path((user_id, token_id)): Path<(String, String)>, Path((user_id, token_id)): Path<(String, String)>,
) -> Result<Redirect, rustical_store::Error> { ) -> Result<Redirect, rustical_store::Error> {

View File

@@ -10,7 +10,7 @@ use axum::{
use axum_extra::TypedHeader; use axum_extra::TypedHeader;
use headers::Referer; use headers::Referer;
use http::StatusCode; use http::StatusCode;
use rustical_store::{Calendar, CalendarStore, auth::User}; use rustical_store::{Calendar, CalendarStore, auth::Principal};
#[derive(Template, WebTemplate)] #[derive(Template, WebTemplate)]
#[template(path = "pages/calendar.html")] #[template(path = "pages/calendar.html")]
@@ -21,7 +21,7 @@ struct CalendarPage {
pub async fn route_calendar<C: CalendarStore>( pub async fn route_calendar<C: CalendarStore>(
Path((owner, cal_id)): Path<(String, String)>, Path((owner, cal_id)): Path<(String, String)>,
Extension(store): Extension<Arc<C>>, Extension(store): Extension<Arc<C>>,
user: User, user: Principal,
) -> Result<Response, rustical_store::Error> { ) -> Result<Response, rustical_store::Error> {
if !user.is_principal(&owner) { if !user.is_principal(&owner) {
return Ok(StatusCode::UNAUTHORIZED.into_response()); return Ok(StatusCode::UNAUTHORIZED.into_response());
@@ -35,7 +35,7 @@ pub async fn route_calendar<C: CalendarStore>(
pub async fn route_calendar_restore<CS: CalendarStore>( pub async fn route_calendar_restore<CS: CalendarStore>(
Path((owner, cal_id)): Path<(String, String)>, Path((owner, cal_id)): Path<(String, String)>,
Extension(store): Extension<Arc<CS>>, Extension(store): Extension<Arc<CS>>,
user: User, user: Principal,
referer: Option<TypedHeader<Referer>>, referer: Option<TypedHeader<Referer>>,
) -> Result<Response, rustical_store::Error> { ) -> Result<Response, rustical_store::Error> {
if !user.is_principal(&owner) { if !user.is_principal(&owner) {
@@ -51,7 +51,7 @@ pub async fn route_calendar_restore<CS: CalendarStore>(
pub async fn route_delete_calendar<C: CalendarStore>( pub async fn route_delete_calendar<C: CalendarStore>(
Path((owner, cal_id)): Path<(String, String)>, Path((owner, cal_id)): Path<(String, String)>,
Extension(store): Extension<Arc<C>>, Extension(store): Extension<Arc<C>>,
user: User, user: Principal,
) -> Result<Response, rustical_store::Error> { ) -> Result<Response, rustical_store::Error> {
if !user.is_principal(&owner) { if !user.is_principal(&owner) {
return Ok(StatusCode::UNAUTHORIZED.into_response()); return Ok(StatusCode::UNAUTHORIZED.into_response());

View File

@@ -12,13 +12,13 @@ use headers::UserAgent;
use http::StatusCode; use http::StatusCode;
use rustical_store::{ use rustical_store::{
Addressbook, AddressbookStore, Calendar, CalendarStore, Addressbook, AddressbookStore, Calendar, CalendarStore,
auth::{AuthenticationProvider, User, user::AppToken}, auth::{AppToken, AuthenticationProvider, Principal},
}; };
#[derive(Template, WebTemplate)] #[derive(Template, WebTemplate)]
#[template(path = "pages/user.html")] #[template(path = "pages/user.html")]
pub struct UserPage { pub struct UserPage {
pub user: User, pub user: Principal,
pub app_tokens: Vec<AppToken>, pub app_tokens: Vec<AppToken>,
pub calendars: Vec<Calendar>, pub calendars: Vec<Calendar>,
pub deleted_calendars: Vec<Calendar>, pub deleted_calendars: Vec<Calendar>,
@@ -39,7 +39,7 @@ pub async fn route_user_named<
Extension(auth_provider): Extension<Arc<AP>>, Extension(auth_provider): Extension<Arc<AP>>,
TypedHeader(user_agent): TypedHeader<UserAgent>, TypedHeader(user_agent): TypedHeader<UserAgent>,
Host(host): Host, Host(host): Host,
user: User, user: Principal,
) -> impl IntoResponse { ) -> impl IntoResponse {
if user_id != user.id { if user_id != user.id {
return StatusCode::UNAUTHORIZED.into_response(); return StatusCode::UNAUTHORIZED.into_response();
@@ -81,11 +81,11 @@ pub async fn route_user_named<
.into_response() .into_response()
} }
pub async fn route_get_home(user: User) -> Redirect { pub async fn route_get_home(user: Principal) -> Redirect {
Redirect::to(&format!("/frontend/user/{}", user.id)) Redirect::to(&format!("/frontend/user/{}", user.id))
} }
pub async fn route_root(user: Option<User>) -> Redirect { pub async fn route_root(user: Option<Principal>) -> Redirect {
match user { match user {
Some(user) => route_get_home(user).await, Some(user) => route_get_home(user).await,
None => Redirect::to("/frontend/login"), None => Redirect::to("/frontend/login"),

View File

@@ -138,7 +138,8 @@ pub async fn route_post_oidc(
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
pub struct AuthCallbackQuery { pub struct AuthCallbackQuery {
code: AuthorizationCode, code: AuthorizationCode,
iss: IssuerUrl, // RFC 9207
iss: Option<IssuerUrl>,
state: String, state: String,
} }
@@ -153,7 +154,9 @@ pub async fn route_get_oidc_callback<US: UserStore + Clone>(
) -> Result<Response, OidcError> { ) -> Result<Response, OidcError> {
let callback_uri = format!("https://{host}/frontend/login/oidc/callback"); let callback_uri = format!("https://{host}/frontend/login/oidc/callback");
assert_eq!(iss, oidc_config.issuer); if let Some(iss) = iss {
assert_eq!(iss, oidc_config.issuer);
}
let oidc_state = session let oidc_state = session
.remove::<OidcState>(SESSION_KEY_OIDC_STATE) .remove::<OidcState>(SESSION_KEY_OIDC_STATE)
.await? .await?

View File

@@ -1,17 +1,26 @@
pub mod middleware; pub mod middleware;
pub mod user; mod principal;
use crate::error::Error; use crate::error::Error;
use async_trait::async_trait; use async_trait::async_trait;
pub use principal::{AppToken, Principal, PrincipalType};
#[async_trait] #[async_trait]
pub trait AuthenticationProvider: Send + Sync + 'static { pub trait AuthenticationProvider: Send + Sync + 'static {
async fn get_principals(&self) -> Result<Vec<User>, crate::Error>; async fn get_principals(&self) -> Result<Vec<Principal>, crate::Error>;
async fn get_principal(&self, id: &str) -> Result<Option<User>, crate::Error>; async fn get_principal(&self, id: &str) -> Result<Option<Principal>, crate::Error>;
async fn remove_principal(&self, id: &str) -> Result<(), crate::Error>; async fn remove_principal(&self, id: &str) -> Result<(), crate::Error>;
async fn insert_principal(&self, user: User, overwrite: bool) -> Result<(), crate::Error>; async fn insert_principal(&self, user: Principal, overwrite: bool) -> Result<(), crate::Error>;
async fn validate_password(&self, user_id: &str, password: &str) async fn validate_password(
-> Result<Option<User>, Error>; &self,
async fn validate_app_token(&self, user_id: &str, token: &str) -> Result<Option<User>, Error>; user_id: &str,
password: &str,
) -> Result<Option<Principal>, Error>;
async fn validate_app_token(
&self,
user_id: &str,
token: &str,
) -> Result<Option<Principal>, Error>;
/// Returns a token identifier /// Returns a token identifier
async fn add_app_token( async fn add_app_token(
&self, &self,
@@ -28,5 +37,3 @@ pub trait AuthenticationProvider: Send + Sync + 'static {
} }
pub use middleware::AuthenticationMiddleware; pub use middleware::AuthenticationMiddleware;
use user::AppToken;
pub use user::User;

View File

@@ -78,8 +78,7 @@ pub struct AppToken {
#[derive(Debug, Clone, Deserialize, Serialize)] #[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
// TODO: Rename this to Principal pub struct Principal {
pub struct User {
pub id: String, pub id: String,
pub displayname: Option<String>, pub displayname: Option<String>,
#[serde(default)] #[serde(default)]
@@ -89,7 +88,7 @@ pub struct User {
pub memberships: Vec<String>, pub memberships: Vec<String>,
} }
impl User { impl Principal {
/// Returns true if the user is either /// Returns true if the user is either
/// - the principal itself /// - the principal itself
/// - has full access to the prinicpal (is member) /// - has full access to the prinicpal (is member)
@@ -114,7 +113,7 @@ impl User {
} }
} }
impl rustical_dav::Principal for User { impl rustical_dav::Principal for Principal {
fn get_id(&self) -> &str { fn get_id(&self) -> &str {
&self.id &self.id
} }
@@ -134,7 +133,7 @@ impl IntoResponse for UnauthorizedError {
} }
} }
impl<S: Send + Sync + Clone> FromRequestParts<S> for User { impl<S: Send + Sync + Clone> FromRequestParts<S> for Principal {
type Rejection = UnauthorizedError; type Rejection = UnauthorizedError;
async fn from_request_parts( async fn from_request_parts(
@@ -149,7 +148,7 @@ impl<S: Send + Sync + Clone> FromRequestParts<S> for User {
} }
} }
impl<S: Send + Sync + Clone> OptionalFromRequestParts<S> for User { impl<S: Send + Sync + Clone> OptionalFromRequestParts<S> for Principal {
type Rejection = Infallible; type Rejection = Infallible;
async fn from_request_parts( async fn from_request_parts(

View File

@@ -58,6 +58,7 @@ pub trait CalendarStore: Send + Sync + 'static {
principal: &str, principal: &str,
cal_id: &str, cal_id: &str,
object_id: &str, object_id: &str,
show_deleted: bool,
) -> Result<CalendarObject, Error>; ) -> Result<CalendarObject, Error>;
async fn put_object( async fn put_object(
&self, &self,

View File

@@ -80,11 +80,13 @@ impl<CS: CalendarStore, BS: CalendarStore> CalendarStore for CombinedCalendarSto
use_trashbin: bool, use_trashbin: bool,
) -> Result<(), Error> { ) -> Result<(), Error> {
if cal_id.starts_with(BIRTHDAYS_PREFIX) { if cal_id.starts_with(BIRTHDAYS_PREFIX) {
Err(Error::ReadOnly)
} else {
self.birthday_store self.birthday_store
.delete_object(principal, cal_id, object_id, use_trashbin) .delete_object(principal, cal_id, object_id, use_trashbin)
.await .await
} else {
self.cal_store
.delete_object(principal, cal_id, object_id, use_trashbin)
.await
} }
} }
@@ -94,14 +96,15 @@ impl<CS: CalendarStore, BS: CalendarStore> CalendarStore for CombinedCalendarSto
principal: &str, principal: &str,
cal_id: &str, cal_id: &str,
object_id: &str, object_id: &str,
show_deleted: bool,
) -> Result<CalendarObject, Error> { ) -> Result<CalendarObject, Error> {
if cal_id.starts_with(BIRTHDAYS_PREFIX) { if cal_id.starts_with(BIRTHDAYS_PREFIX) {
self.birthday_store self.birthday_store
.get_object(principal, cal_id, object_id) .get_object(principal, cal_id, object_id, show_deleted)
.await .await
} else { } else {
self.cal_store self.cal_store
.get_object(principal, cal_id, object_id) .get_object(principal, cal_id, object_id, show_deleted)
.await .await
} }
} }
@@ -237,4 +240,3 @@ impl<CS: CalendarStore, BS: CalendarStore> CalendarStore for CombinedCalendarSto
} }
} }
} }

View File

@@ -126,13 +126,14 @@ impl<AS: AddressbookStore> CalendarStore for ContactBirthdayStore<AS> {
principal: &str, principal: &str,
cal_id: &str, cal_id: &str,
object_id: &str, object_id: &str,
show_deleted: bool,
) -> Result<CalendarObject, Error> { ) -> Result<CalendarObject, Error> {
let cal_id = cal_id let cal_id = cal_id
.strip_prefix(BIRTHDAYS_PREFIX) .strip_prefix(BIRTHDAYS_PREFIX)
.ok_or(Error::NotFound)?; .ok_or(Error::NotFound)?;
let (addressobject_id, date_type) = object_id.rsplit_once("-").ok_or(Error::NotFound)?; let (addressobject_id, date_type) = object_id.rsplit_once("-").ok_or(Error::NotFound)?;
self.0 self.0
.get_object(principal, cal_id, addressobject_id, false) .get_object(principal, cal_id, addressobject_id, show_deleted)
.await? .await?
.get_significant_dates()? .get_significant_dates()?
.remove(date_type) .remove(date_type)

View File

@@ -22,22 +22,15 @@ pub use addressbook::Addressbook;
pub use calendar::Calendar; pub use calendar::Calendar;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum CollectionOperationType { pub enum CollectionOperationInfo {
// Sync-Token increased // Sync-Token increased
Object, Content { sync_token: String },
// Collection deleted
Delete, Delete,
} }
#[derive(Debug, Clone)]
pub enum CollectionOperationDomain {
Calendar,
Addressbook,
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct CollectionOperation { pub struct CollectionOperation {
pub r#type: CollectionOperationType,
pub domain: CollectionOperationDomain,
pub topic: String, pub topic: String,
pub sync_token: Option<String>, pub data: CollectionOperationInfo,
} }

View File

@@ -3,8 +3,8 @@ use async_trait::async_trait;
use derive_more::derive::Constructor; use derive_more::derive::Constructor;
use rustical_ical::AddressObject; use rustical_ical::AddressObject;
use rustical_store::{ use rustical_store::{
Addressbook, AddressbookStore, CollectionOperation, CollectionOperationDomain, Addressbook, AddressbookStore, CollectionOperation, CollectionOperationInfo, Error,
CollectionOperationType, Error, synctoken::format_synctoken, synctoken::format_synctoken,
}; };
use sqlx::{Acquire, Executor, Sqlite, SqlitePool, Transaction}; use sqlx::{Acquire, Executor, Sqlite, SqlitePool, Transaction};
use tokio::sync::mpsc::Sender; use tokio::sync::mpsc::Sender;
@@ -250,7 +250,7 @@ impl SqliteAddressbookStore {
) -> Result<AddressObject, rustical_store::Error> { ) -> Result<AddressObject, rustical_store::Error> {
Ok(sqlx::query_as!( Ok(sqlx::query_as!(
AddressObjectRow, AddressObjectRow,
"SELECT id, vcf FROM addressobjects WHERE (principal, addressbook_id, id) = (?, ?, ?) AND ((deleted_at IS NULL) or ?)", "SELECT id, vcf FROM addressobjects WHERE (principal, addressbook_id, id) = (?, ?, ?) AND ((deleted_at IS NULL) OR ?)",
principal, principal,
addressbook_id, addressbook_id,
object_id, object_id,
@@ -413,10 +413,8 @@ impl AddressbookStore for SqliteAddressbookStore {
if let Some(addressbook) = addressbook { if let Some(addressbook) = addressbook {
if let Err(err) = self.sender.try_send(CollectionOperation { if let Err(err) = self.sender.try_send(CollectionOperation {
r#type: CollectionOperationType::Delete, data: CollectionOperationInfo::Delete,
domain: CollectionOperationDomain::Addressbook,
topic: addressbook.push_topic, topic: addressbook.push_topic,
sync_token: None,
}) { }) {
error!("Push notification about deleted addressbook failed: {err}"); error!("Push notification about deleted addressbook failed: {err}");
}; };
@@ -485,7 +483,7 @@ impl AddressbookStore for SqliteAddressbookStore {
) )
.await?; .await?;
let synctoken = log_object_operation( let sync_token = log_object_operation(
&mut tx, &mut tx,
&principal, &principal,
&addressbook_id, &addressbook_id,
@@ -498,13 +496,11 @@ impl AddressbookStore for SqliteAddressbookStore {
tx.commit().await.map_err(crate::Error::from)?; tx.commit().await.map_err(crate::Error::from)?;
if let Err(err) = self.sender.try_send(CollectionOperation { if let Err(err) = self.sender.try_send(CollectionOperation {
r#type: CollectionOperationType::Object, data: CollectionOperationInfo::Content { sync_token },
domain: CollectionOperationDomain::Addressbook,
topic: self topic: self
.get_addressbook(&principal, &addressbook_id, false) .get_addressbook(&principal, &addressbook_id, false)
.await? .await?
.push_topic, .push_topic,
sync_token: Some(synctoken),
}) { }) {
error!("Push notification about deleted addressbook failed: {err}"); error!("Push notification about deleted addressbook failed: {err}");
}; };
@@ -524,7 +520,7 @@ impl AddressbookStore for SqliteAddressbookStore {
Self::_delete_object(&mut *tx, principal, addressbook_id, object_id, use_trashbin).await?; Self::_delete_object(&mut *tx, principal, addressbook_id, object_id, use_trashbin).await?;
let synctoken = log_object_operation( let sync_token = log_object_operation(
&mut tx, &mut tx,
principal, principal,
addressbook_id, addressbook_id,
@@ -536,16 +532,15 @@ impl AddressbookStore for SqliteAddressbookStore {
tx.commit().await.map_err(crate::Error::from)?; tx.commit().await.map_err(crate::Error::from)?;
// TODO: Watch for errors here? if let Err(err) = self.sender.try_send(CollectionOperation {
let _ = self.sender.try_send(CollectionOperation { data: CollectionOperationInfo::Content { sync_token },
r#type: CollectionOperationType::Object,
domain: CollectionOperationDomain::Addressbook,
topic: self topic: self
.get_addressbook(principal, addressbook_id, false) .get_addressbook(principal, addressbook_id, false)
.await? .await?
.push_topic, .push_topic,
sync_token: Some(synctoken), }) {
}); error!("Push notification about deleted addressbook failed: {err}");
};
Ok(()) Ok(())
} }
@@ -560,7 +555,7 @@ impl AddressbookStore for SqliteAddressbookStore {
Self::_restore_object(&mut *tx, principal, addressbook_id, object_id).await?; Self::_restore_object(&mut *tx, principal, addressbook_id, object_id).await?;
let synctoken = log_object_operation( let sync_token = log_object_operation(
&mut tx, &mut tx,
principal, principal,
addressbook_id, addressbook_id,
@@ -571,16 +566,15 @@ impl AddressbookStore for SqliteAddressbookStore {
.map_err(crate::Error::from)?; .map_err(crate::Error::from)?;
tx.commit().await.map_err(crate::Error::from)?; tx.commit().await.map_err(crate::Error::from)?;
// TODO: Watch for errors here? if let Err(err) = self.sender.try_send(CollectionOperation {
let _ = self.sender.try_send(CollectionOperation { data: CollectionOperationInfo::Content { sync_token },
r#type: CollectionOperationType::Object,
domain: CollectionOperationDomain::Addressbook,
topic: self topic: self
.get_addressbook(principal, addressbook_id, false) .get_addressbook(principal, addressbook_id, false)
.await? .await?
.push_topic, .push_topic,
sync_token: Some(synctoken), }) {
}); error!("Push notification about deleted addressbook failed: {err}");
};
Ok(()) Ok(())
} }

View File

@@ -6,7 +6,7 @@ use rustical_ical::{CalDateTime, CalendarObject, CalendarObjectType};
use rustical_store::calendar_store::CalendarQuery; use rustical_store::calendar_store::CalendarQuery;
use rustical_store::synctoken::format_synctoken; use rustical_store::synctoken::format_synctoken;
use rustical_store::{Calendar, CalendarStore, Error}; use rustical_store::{Calendar, CalendarStore, Error};
use rustical_store::{CollectionOperation, CollectionOperationType}; use rustical_store::{CollectionOperation, CollectionOperationInfo};
use sqlx::types::chrono::NaiveDateTime; use sqlx::types::chrono::NaiveDateTime;
use sqlx::{Acquire, Executor, Sqlite, SqlitePool, Transaction}; use sqlx::{Acquire, Executor, Sqlite, SqlitePool, Transaction};
use tokio::sync::mpsc::Sender; use tokio::sync::mpsc::Sender;
@@ -296,13 +296,15 @@ impl SqliteCalendarStore {
principal: &str, principal: &str,
cal_id: &str, cal_id: &str,
object_id: &str, object_id: &str,
show_deleted: bool,
) -> Result<CalendarObject, Error> { ) -> Result<CalendarObject, Error> {
sqlx::query_as!( sqlx::query_as!(
CalendarObjectRow, CalendarObjectRow,
"SELECT id, ics FROM calendarobjects WHERE (principal, cal_id, id) = (?, ?, ?)", "SELECT id, ics FROM calendarobjects WHERE (principal, cal_id, id) = (?, ?, ?) AND ((deleted_at IS NULL) OR ?)",
principal, principal,
cal_id, cal_id,
object_id object_id,
show_deleted
) )
.fetch_one(executor) .fetch_one(executor)
.await .await
@@ -454,7 +456,7 @@ impl SqliteCalendarStore {
.unwrap_or(0); .unwrap_or(0);
for Row { object_id, .. } in changes { for Row { object_id, .. } in changes {
match Self::_get_object(&mut *conn, principal, cal_id, &object_id).await { match Self::_get_object(&mut *conn, principal, cal_id, &object_id, false).await {
Ok(object) => objects.push(object), Ok(object) => objects.push(object),
Err(rustical_store::Error::NotFound) => deleted_objects.push(object_id), Err(rustical_store::Error::NotFound) => deleted_objects.push(object_id),
Err(err) => return Err(err), Err(err) => return Err(err),
@@ -518,10 +520,8 @@ impl CalendarStore for SqliteCalendarStore {
if let Some(cal) = cal { if let Some(cal) = cal {
if let Err(err) = self.sender.try_send(CollectionOperation { if let Err(err) = self.sender.try_send(CollectionOperation {
r#type: CollectionOperationType::Delete, data: CollectionOperationInfo::Delete,
domain: rustical_store::CollectionOperationDomain::Calendar,
topic: cal.push_topic, topic: cal.push_topic,
sync_token: None,
}) { }) {
error!("Push notification about deleted calendar failed: {err}"); error!("Push notification about deleted calendar failed: {err}");
}; };
@@ -559,8 +559,9 @@ impl CalendarStore for SqliteCalendarStore {
principal: &str, principal: &str,
cal_id: &str, cal_id: &str,
object_id: &str, object_id: &str,
show_deleted: bool,
) -> Result<CalendarObject, Error> { ) -> Result<CalendarObject, Error> {
Self::_get_object(&self.db, principal, cal_id, object_id).await Self::_get_object(&self.db, principal, cal_id, object_id, show_deleted).await
} }
#[instrument] #[instrument]
@@ -585,7 +586,7 @@ impl CalendarStore for SqliteCalendarStore {
) )
.await?; .await?;
let synctoken = log_object_operation( let sync_token = log_object_operation(
&mut tx, &mut tx,
&principal, &principal,
&cal_id, &cal_id,
@@ -597,10 +598,8 @@ impl CalendarStore for SqliteCalendarStore {
tx.commit().await.map_err(crate::Error::from)?; tx.commit().await.map_err(crate::Error::from)?;
if let Err(err) = self.sender.try_send(CollectionOperation { if let Err(err) = self.sender.try_send(CollectionOperation {
r#type: CollectionOperationType::Object, data: CollectionOperationInfo::Content { sync_token },
domain: rustical_store::CollectionOperationDomain::Calendar,
topic: self.get_calendar(&principal, &cal_id).await?.push_topic, topic: self.get_calendar(&principal, &cal_id).await?.push_topic,
sync_token: Some(synctoken),
}) { }) {
error!("Push notification about deleted calendar failed: {err}"); error!("Push notification about deleted calendar failed: {err}");
}; };
@@ -619,15 +618,13 @@ impl CalendarStore for SqliteCalendarStore {
Self::_delete_object(&mut *tx, principal, cal_id, id, use_trashbin).await?; Self::_delete_object(&mut *tx, principal, cal_id, id, use_trashbin).await?;
let synctoken = let sync_token =
log_object_operation(&mut tx, principal, cal_id, id, ChangeOperation::Delete).await?; log_object_operation(&mut tx, principal, cal_id, id, ChangeOperation::Delete).await?;
tx.commit().await.map_err(crate::Error::from)?; tx.commit().await.map_err(crate::Error::from)?;
if let Err(err) = self.sender.try_send(CollectionOperation { if let Err(err) = self.sender.try_send(CollectionOperation {
r#type: CollectionOperationType::Object, data: CollectionOperationInfo::Content { sync_token },
domain: rustical_store::CollectionOperationDomain::Calendar,
topic: self.get_calendar(principal, cal_id).await?.push_topic, topic: self.get_calendar(principal, cal_id).await?.push_topic,
sync_token: Some(synctoken),
}) { }) {
error!("Push notification about deleted calendar failed: {err}"); error!("Push notification about deleted calendar failed: {err}");
}; };
@@ -645,16 +642,14 @@ impl CalendarStore for SqliteCalendarStore {
Self::_restore_object(&mut *tx, principal, cal_id, object_id).await?; Self::_restore_object(&mut *tx, principal, cal_id, object_id).await?;
let synctoken = let sync_token =
log_object_operation(&mut tx, principal, cal_id, object_id, ChangeOperation::Add) log_object_operation(&mut tx, principal, cal_id, object_id, ChangeOperation::Add)
.await?; .await?;
tx.commit().await.map_err(crate::Error::from)?; tx.commit().await.map_err(crate::Error::from)?;
if let Err(err) = self.sender.try_send(CollectionOperation { if let Err(err) = self.sender.try_send(CollectionOperation {
r#type: CollectionOperationType::Object, data: CollectionOperationInfo::Content { sync_token },
domain: rustical_store::CollectionOperationDomain::Calendar,
topic: self.get_calendar(principal, cal_id).await?.push_topic, topic: self.get_calendar(principal, cal_id).await?.push_topic,
sync_token: Some(synctoken),
}) { }) {
error!("Push notification about deleted calendar failed: {err}"); error!("Push notification about deleted calendar failed: {err}");
}; };

View File

@@ -7,7 +7,7 @@ use pbkdf2::{
}; };
use rustical_store::{ use rustical_store::{
Error, Secret, Error, Secret,
auth::{AuthenticationProvider, User, user::AppToken}, auth::{AppToken, AuthenticationProvider, Principal},
}; };
use sqlx::{SqlitePool, types::Json}; use sqlx::{SqlitePool, types::Json};
use tracing::instrument; use tracing::instrument;
@@ -21,11 +21,11 @@ struct PrincipalRow {
memberships: Option<Json<Vec<Option<String>>>>, memberships: Option<Json<Vec<Option<String>>>>,
} }
impl TryFrom<PrincipalRow> for User { impl TryFrom<PrincipalRow> for Principal {
type Error = Error; type Error = Error;
fn try_from(value: PrincipalRow) -> Result<Self, Self::Error> { fn try_from(value: PrincipalRow) -> Result<Self, Self::Error> {
Ok(User { Ok(Principal {
id: value.id, id: value.id,
displayname: value.displayname, displayname: value.displayname,
password: value.password_hash.map(Secret::from), password: value.password_hash.map(Secret::from),
@@ -49,8 +49,8 @@ pub struct SqlitePrincipalStore {
#[async_trait] #[async_trait]
impl AuthenticationProvider for SqlitePrincipalStore { impl AuthenticationProvider for SqlitePrincipalStore {
#[instrument] #[instrument]
async fn get_principals(&self) -> Result<Vec<User>, Error> { async fn get_principals(&self) -> Result<Vec<Principal>, Error> {
let result: Result<Vec<User>, Error> = sqlx::query_as!( let result: Result<Vec<Principal>, Error> = sqlx::query_as!(
PrincipalRow, PrincipalRow,
r#" r#"
SELECT id, displayname, principal_type, password_hash, json_group_array(member_of) AS "memberships: Json<Vec<Option<String>>>" SELECT id, displayname, principal_type, password_hash, json_group_array(member_of) AS "memberships: Json<Vec<Option<String>>>"
@@ -63,13 +63,13 @@ impl AuthenticationProvider for SqlitePrincipalStore {
.await .await
.map_err(crate::Error::from)? .map_err(crate::Error::from)?
.into_iter() .into_iter()
.map(User::try_from) .map(Principal::try_from)
.collect(); .collect();
Ok(result?) Ok(result?)
} }
#[instrument] #[instrument]
async fn get_principal(&self, id: &str) -> Result<Option<User>, Error> { async fn get_principal(&self, id: &str) -> Result<Option<Principal>, Error> {
let row= sqlx::query_as!( let row= sqlx::query_as!(
PrincipalRow, PrincipalRow,
r#" r#"
@@ -83,7 +83,7 @@ impl AuthenticationProvider for SqlitePrincipalStore {
.fetch_optional(&self.db) .fetch_optional(&self.db)
.await .await
.map_err(crate::Error::from)? .map_err(crate::Error::from)?
.map(User::try_from); .map(Principal::try_from);
if let Some(row) = row { if let Some(row) = row {
Ok(Some(row?)) Ok(Some(row?))
} else { } else {
@@ -103,7 +103,7 @@ impl AuthenticationProvider for SqlitePrincipalStore {
#[instrument] #[instrument]
async fn insert_principal( async fn insert_principal(
&self, &self,
user: User, user: Principal,
overwrite: bool, overwrite: bool,
) -> Result<(), rustical_store::Error> { ) -> Result<(), rustical_store::Error> {
// Would be cleaner to put this into a transaction but for now it will be fine // Would be cleaner to put this into a transaction but for now it will be fine
@@ -142,7 +142,11 @@ impl AuthenticationProvider for SqlitePrincipalStore {
} }
#[instrument(skip(token))] #[instrument(skip(token))]
async fn validate_app_token(&self, user_id: &str, token: &str) -> Result<Option<User>, Error> { async fn validate_app_token(
&self,
user_id: &str,
token: &str,
) -> Result<Option<Principal>, Error> {
for app_token in &self.get_app_tokens(user_id).await? { for app_token in &self.get_app_tokens(user_id).await? {
if password_auth::verify_password(token, app_token.token.as_ref()).is_ok() { if password_auth::verify_password(token, app_token.token.as_ref()).is_ok() {
return self.get_principal(user_id).await; return self.get_principal(user_id).await;
@@ -169,8 +173,8 @@ impl AuthenticationProvider for SqlitePrincipalStore {
&self, &self,
user_id: &str, user_id: &str,
password_input: &str, password_input: &str,
) -> Result<Option<User>, Error> { ) -> Result<Option<Principal>, Error> {
let user: User = match self.get_principal(user_id).await? { let user: Principal = match self.get_principal(user_id).await? {
Some(user) => user, Some(user) => user,
None => return Ok(None), None => return Ok(None),
}; };

View File

@@ -4,13 +4,15 @@ a CalDAV/CardDAV server
!!! warning !!! warning
RustiCal is **not production-ready!** RustiCal is **not production-ready!**
While I've started migrating to RustiCal and becoming more confident, please know that bugs and rough edges will still occur. I've been using it for the last few weeks and I'm slowly becoming more confident,
however you'd be one of the first testers so expect bugs and rough edges.
If you still want to play around with it in its current state, absolutely feel free to do so and to open up an issue if something is not working. :) If you still want to play around with it in its current state, absolutely feel free to do so and to open up an issue if something is not working. :)
## Features ## Features
- easy to backup, everything saved in one SQLite database - easy to backup, everything saved in one SQLite database
- also export feature in the frontend - also export feature in the frontend
- [WebDAV Push](https://github.com/bitfireAT/webdav-push/) support, so near-instant synchronisation to DAVx5
- lightweight (the container image contains only one binary) - lightweight (the container image contains only one binary)
- adequately fast (I'd love to say blazingly fast™ :fire: but I don't have any benchmarks) - adequately fast (I'd love to say blazingly fast™ :fire: but I don't have any benchmarks)
- deleted calendars are recoverable - deleted calendars are recoverable

View File

@@ -34,6 +34,7 @@ pub fn make_app<AS: AddressbookStore, CS: CalendarStore, S: SubscriptionStore>(
frontend_config: FrontendConfig, frontend_config: FrontendConfig,
oidc_config: Option<OidcConfig>, oidc_config: Option<OidcConfig>,
nextcloud_login_config: NextcloudLoginConfig, nextcloud_login_config: NextcloudLoginConfig,
dav_push_enabled: bool,
) -> Router<()> { ) -> Router<()> {
let combined_cal_store = Arc::new(CombinedCalendarStore::new( let combined_cal_store = Arc::new(CombinedCalendarStore::new(
cal_store.clone(), cal_store.clone(),
@@ -90,6 +91,13 @@ pub fn make_app<AS: AddressbookStore, CS: CalendarStore, S: SubscriptionStore>(
nextcloud_login_router(auth_provider.clone()), nextcloud_login_router(auth_provider.clone()),
); );
} }
if dav_push_enabled {
router = router.merge(rustical_dav_push::subscription_service(
subscription_store.clone(),
));
}
router router
.layer( .layer(
SessionManagerLayer::new(session_store) SessionManagerLayer::new(session_store)
@@ -126,12 +134,18 @@ pub fn make_app<AS: AddressbookStore, CS: CalendarStore, S: SubscriptionStore>(
if response.status().is_server_error() { if response.status().is_server_error() {
tracing::error!("server error"); tracing::error!("server error");
} else if response.status().is_client_error() { } else if response.status().is_client_error() {
if response.status() == StatusCode::UNAUTHORIZED { match response.status() {
// The iOS client always tries an unauthenticated request first so StatusCode::UNAUTHORIZED => {
// logging 401's as errors would clog up our logs // The iOS client always tries an unauthenticated request first so
tracing::debug!("unauthorized"); // logging 401's as errors would clog up our logs
} else { tracing::debug!("unauthorized");
tracing::error!("client error"); }
StatusCode::NOT_FOUND => {
tracing::warn!("client error");
}
_ => {
tracing::error!("client error");
}
} }
}; };
}) })

View File

@@ -6,7 +6,7 @@ use figment::{
providers::{Env, Format, Toml}, providers::{Env, Format, Toml},
}; };
use password_hash::{PasswordHasher, SaltString, rand_core::OsRng}; use password_hash::{PasswordHasher, SaltString, rand_core::OsRng};
use rustical_store::auth::{AuthenticationProvider, User, user::PrincipalType}; use rustical_store::auth::{AuthenticationProvider, Principal, PrincipalType};
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
pub struct PrincipalsArgs { pub struct PrincipalsArgs {
@@ -99,7 +99,7 @@ pub async fn cmd_principals(args: PrincipalsArgs) -> anyhow::Result<()> {
}; };
principal_store principal_store
.insert_principal( .insert_principal(
User { Principal {
id, id,
displayname: name, displayname: name,
principal_type: principal_type.unwrap_or_default(), principal_type: principal_type.unwrap_or_default(),

View File

@@ -37,9 +37,14 @@ pub struct TracingConfig {
pub opentelemetry: bool, pub opentelemetry: bool,
} }
fn default_true() -> bool {
true
}
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields, default)] #[serde(deny_unknown_fields, default)]
pub struct DavPushConfig { pub struct DavPushConfig {
#[serde(default = "default_true")]
pub enabled: bool, pub enabled: bool,
#[serde(default)] #[serde(default)]
// Allowed Push servers, accepts any by default // Allowed Push servers, accepts any by default

View File

@@ -117,6 +117,7 @@ async fn main() -> Result<()> {
config.frontend.clone(), config.frontend.clone(),
config.oidc.clone(), config.oidc.clone(),
config.nextcloud_login.clone(), config.nextcloud_login.clone(),
config.dav_push.enabled,
); );
let app = ServiceExt::<Request>::into_make_service( let app = ServiceExt::<Request>::into_make_service(
NormalizePathLayer::trim_trailing_slash().layer(app), NormalizePathLayer::trim_trailing_slash().layer(app),