mirror of
https://github.com/lennart-k/rustical.git
synced 2025-12-13 20:32:48 +00:00
Compare commits
76 Commits
dav-push
...
feature/ca
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
41039242ee | ||
|
|
a20e9800bd | ||
|
|
80cca7b7b2 | ||
|
|
f04987a171 | ||
|
|
3eeef18a14 | ||
|
|
32225bdda8 | ||
|
|
103ac0b1f9 | ||
|
|
300a0024ee | ||
|
|
0dbc05345b | ||
|
|
b5f23b0f9b | ||
|
|
5ee789bec1 | ||
|
|
49aab931d0 | ||
|
|
7628cdafbd | ||
|
|
6d6f8f20df | ||
|
|
fc590976bc | ||
|
|
71c2f8c019 | ||
|
|
0595920809 | ||
|
|
0feaaaaca1 | ||
|
|
e000165555 | ||
|
|
487e99216a | ||
|
|
38dcf88f24 | ||
|
|
2ce0c00f89 | ||
|
|
38de0ab268 | ||
|
|
9dd5995950 | ||
|
|
2ba0beeafc | ||
|
|
8f29a468db | ||
|
|
764d049d3c | ||
|
|
720e6f6115 | ||
|
|
d5b43b33f4 | ||
|
|
6ae2276035 | ||
|
|
152bf374d7 | ||
|
|
61f14ca072 | ||
|
|
6bcad7cc65 | ||
|
|
e58973d366 | ||
|
|
573781310a | ||
|
|
bbe9113f5c | ||
|
|
ac1dbb29d8 | ||
|
|
1d25d6cc70 | ||
|
|
c05c330601 | ||
|
|
00eb43f048 | ||
|
|
95889e3df1 | ||
|
|
790c657b08 | ||
|
|
57832116aa | ||
|
|
0c6aef7c06 | ||
|
|
22ed278dbb | ||
|
|
1a827a164f | ||
|
|
e57a14cad1 | ||
|
|
5ad6ee2e99 | ||
|
|
c14f98a432 | ||
|
|
7f3ce01c2b | ||
|
|
5a6ffd3c19 | ||
|
|
cf3e213894 | ||
|
|
13128a5caa | ||
|
|
9836a696ad | ||
|
|
05ff2536f6 | ||
|
|
bcc6bef848 | ||
|
|
088b920b68 | ||
|
|
3c9c1c7abf | ||
|
|
b7c24fe2f0 | ||
|
|
08c4bd4289 | ||
|
|
ef33868151 | ||
|
|
0f294cf2e1 | ||
|
|
fb8889b5f6 | ||
|
|
5ebcab7a19 | ||
|
|
3c7ee09116 | ||
|
|
f55224b21a | ||
|
|
0acc3c22d9 | ||
|
|
212274fce9 | ||
|
|
1436af1f9c | ||
|
|
8f69bc839a | ||
|
|
37eb6df64a | ||
|
|
3af9b3b8b4 | ||
|
|
d14ded7179 | ||
|
|
de6ccdc37b | ||
|
|
86ecaef6db | ||
|
|
2686530024 |
2000
Cargo.lock
generated
2000
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
51
Cargo.toml
51
Cargo.toml
@@ -32,11 +32,8 @@ debug = 0
|
||||
[workspace.dependencies]
|
||||
uuid = { version = "1.11", features = ["v4", "fast-rng"] }
|
||||
async-trait = "0.1"
|
||||
actix-web = "4.9"
|
||||
axum = "0.8"
|
||||
tracing = { version = "0.1", features = ["async-await"] }
|
||||
tracing-actix-web = "0.7"
|
||||
actix-session = { version = "0.10", features = ["cookie-session"] }
|
||||
actix-web-httpauth = "0.8"
|
||||
anyhow = { version = "1.0", features = ["backtrace"] }
|
||||
serde = { version = "1.0", features = ["serde_derive", "derive", "rc"] }
|
||||
futures-util = "0.3"
|
||||
@@ -61,9 +58,10 @@ base64 = "0.22"
|
||||
thiserror = "2.0"
|
||||
quick-xml = { version = "0.37" }
|
||||
rust-embed = "8.5"
|
||||
tower-sessions = "0.14"
|
||||
futures-core = "0.3.31"
|
||||
hex = { version = "0.4.3", features = ["serde"] }
|
||||
mime_guess = "2.0.5"
|
||||
mime_guess = "2.0"
|
||||
itertools = "0.14"
|
||||
log = "0.4"
|
||||
derive_more = { version = "2.0", features = [
|
||||
@@ -72,9 +70,10 @@ derive_more = { version = "2.0", features = [
|
||||
"into",
|
||||
"deref",
|
||||
"constructor",
|
||||
"display",
|
||||
] }
|
||||
askama = { version = "0.13", features = ["serde_json"] }
|
||||
askama_web = { version = "0.13.0", features = ["actix-web-4"] }
|
||||
askama = { version = "0.14", features = ["serde_json"] }
|
||||
askama_web = { version = "0.14.0", features = ["axum-0.8"] }
|
||||
sqlx = { version = "0.8", default-features = false, features = [
|
||||
"sqlx-sqlite",
|
||||
"uuid",
|
||||
@@ -85,12 +84,21 @@ sqlx = { version = "0.8", default-features = false, features = [
|
||||
"migrate",
|
||||
"json",
|
||||
] }
|
||||
http = "1.3"
|
||||
headers = "0.4"
|
||||
strum = "0.27"
|
||||
strum_macros = "0.27"
|
||||
serde_json = { version = "1.0", features = ["raw_value"] }
|
||||
sqlx-sqlite = { version = "0.8", features = ["bundled"] }
|
||||
ical = { version = "0.11", features = ["generator", "serde"] }
|
||||
toml = "0.8"
|
||||
tower = "0.5"
|
||||
tower-http = { version = "0.6", features = [
|
||||
"trace",
|
||||
"normalize-path",
|
||||
"catch-panic",
|
||||
] }
|
||||
percent-encoding = "2.3"
|
||||
rustical_dav = { path = "./crates/dav/" }
|
||||
rustical_dav_push = { path = "./crates/dav_push/" }
|
||||
rustical_store = { path = "./crates/store/" }
|
||||
@@ -100,9 +108,12 @@ rustical_carddav = { path = "./crates/carddav/" }
|
||||
rustical_frontend = { path = "./crates/frontend/" }
|
||||
rustical_xml = { path = "./crates/xml/" }
|
||||
rustical_oidc = { path = "./crates/oidc/" }
|
||||
rustical_ical = { path = "./crates/ical/" }
|
||||
chrono-tz = "0.10"
|
||||
chrono-humanize = "0.2"
|
||||
rand = "0.8"
|
||||
rand = "0.9"
|
||||
axum-extra = { version = "0.10", features = ["typed-header"] }
|
||||
rrule = "0.14"
|
||||
argon2 = "0.5"
|
||||
rpassword = "7.3"
|
||||
password-hash = { version = "0.5" }
|
||||
@@ -123,9 +134,8 @@ clap = { version = "4.5", features = ["derive", "env"] }
|
||||
rustical_store = { workspace = true }
|
||||
rustical_store_sqlite = { workspace = true }
|
||||
rustical_caldav = { workspace = true }
|
||||
rustical_carddav = { workspace = true }
|
||||
rustical_carddav.workspace = true
|
||||
rustical_frontend = { workspace = true }
|
||||
actix-web = { workspace = true }
|
||||
toml = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
@@ -134,28 +144,27 @@ anyhow = { workspace = true }
|
||||
clap.workspace = true
|
||||
sqlx = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
tracing-actix-web = { workspace = true }
|
||||
uuid.workspace = true
|
||||
axum.workspace = true
|
||||
|
||||
opentelemetry = { version = "0.29", optional = true }
|
||||
opentelemetry-otlp = { version = "0.29", optional = true, features = [
|
||||
opentelemetry = { version = "0.30", optional = true }
|
||||
opentelemetry-otlp = { version = "0.30", optional = true, features = [
|
||||
"grpc-tonic",
|
||||
] }
|
||||
opentelemetry_sdk = { version = "0.29", features = [
|
||||
opentelemetry_sdk = { version = "0.30", features = [
|
||||
"rt-tokio",
|
||||
], optional = true }
|
||||
opentelemetry-semantic-conventions = { version = "0.29", optional = true }
|
||||
tracing-opentelemetry = { version = "0.30", optional = true }
|
||||
opentelemetry-semantic-conventions = { version = "0.30", optional = true }
|
||||
tracing-opentelemetry = { version = "0.31", optional = true }
|
||||
tracing-subscriber = { version = "0.3", features = [
|
||||
"env-filter",
|
||||
"fmt",
|
||||
"registry",
|
||||
] }
|
||||
|
||||
figment = { version = "0.10", features = ["env", "toml"] }
|
||||
|
||||
rand.workspace = true
|
||||
tower-sessions.workspace = true
|
||||
rpassword.workspace = true
|
||||
tower.workspace = true
|
||||
argon2.workspace = true
|
||||
pbkdf2.workspace = true
|
||||
password-hash.workspace = true
|
||||
@@ -164,3 +173,7 @@ rustical_dav.workspace = true
|
||||
rustical_dav_push.workspace = true
|
||||
rustical_oidc.workspace = true
|
||||
quick-xml.workspace = true
|
||||
tower-http.workspace = true
|
||||
axum-extra.workspace = true
|
||||
headers.workspace = true
|
||||
http.workspace = true
|
||||
|
||||
@@ -42,5 +42,7 @@ FROM scratch
|
||||
COPY --from=builder /usr/local/cargo/bin/rustical /usr/local/bin/rustical
|
||||
CMD ["/usr/local/bin/rustical"]
|
||||
|
||||
ENV RUSTICAL_DATA_STORE__SQLITE__DB_URL=/var/lib/rustical/db.sqlite3
|
||||
|
||||
LABEL org.opencontainers.image.authors="Lennart K github.com/lennart-k"
|
||||
EXPOSE 4000
|
||||
|
||||
2
Justfile
Normal file
2
Justfile
Normal file
@@ -0,0 +1,2 @@
|
||||
licenses:
|
||||
cargo about generate about.hbs > crates/frontend/public/assets/licenses.html
|
||||
@@ -11,7 +11,7 @@ a CalDAV/CardDAV server
|
||||
## Features
|
||||
|
||||
- easy to backup, everything saved in one SQLite database
|
||||
- [WebDAV Push](https://github.com/bitfireAT/webdav-push/) support, so near-instant synchronisation to DAVx5
|
||||
- ~~[WebDAV Push](https://github.com/bitfireAT/webdav-push/) support, so near-instant synchronisation to DAVx5~~ (currently broken)
|
||||
- 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)
|
||||
- deleted calendars are recoverable
|
||||
|
||||
70
about.hbs
Normal file
70
about.hbs
Normal file
@@ -0,0 +1,70 @@
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<style>
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background: #333;
|
||||
color: white;
|
||||
}
|
||||
a {
|
||||
color: skyblue;
|
||||
}
|
||||
}
|
||||
.container {
|
||||
font-family: sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.intro {
|
||||
text-align: center;
|
||||
}
|
||||
.licenses-list {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.license-used-by {
|
||||
margin-top: -10px;
|
||||
}
|
||||
.license-text {
|
||||
max-height: 200px;
|
||||
overflow-y: scroll;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<main class="container">
|
||||
<div class="intro">
|
||||
<h1>Third Party Licenses</h1>
|
||||
<p>This page lists the licenses of packages used by RustiCal.</p>
|
||||
</div>
|
||||
|
||||
<h2>Overview of licenses:</h2>
|
||||
<ul class="licenses-overview">
|
||||
{{#each overview}}
|
||||
<li><a href="#{{id}}">{{name}}</a> ({{count}})</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
|
||||
<h2>All license text:</h2>
|
||||
<ul class="licenses-list">
|
||||
{{#each licenses}}
|
||||
<li class="license">
|
||||
<h3 id="{{id}}">{{name}}</h3>
|
||||
<h4>Used by:</h4>
|
||||
<ul class="license-used-by">
|
||||
{{#each used_by}}
|
||||
<li><a href="{{#if crate.repository}} {{crate.repository}} {{else}} https://crates.io/crates/{{crate.name}} {{/if}}">{{crate.name}} {{crate.version}}</a></li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
<pre class="license-text">{{text}}</pre>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</main>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
11
about.toml
Normal file
11
about.toml
Normal file
@@ -0,0 +1,11 @@
|
||||
accepted = [
|
||||
"Apache-2.0",
|
||||
"MIT",
|
||||
"BSD-3-Clause",
|
||||
"ISC",
|
||||
"Unicode-3.0",
|
||||
"CDLA-Permissive-2.0",
|
||||
"Zlib",
|
||||
"AGPL-3.0",
|
||||
]
|
||||
workarounds = ["ring", "chrono", "rustls"]
|
||||
@@ -7,15 +7,15 @@ repository.workspace = true
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
actix-web = { workspace = true }
|
||||
axum.workspace = true
|
||||
axum-extra.workspace = true
|
||||
tower.workspace = true
|
||||
async-trait = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
quick-xml = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-actix-web = { workspace = true }
|
||||
futures-util = { workspace = true }
|
||||
derive_more = { workspace = true }
|
||||
actix-web-httpauth = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
@@ -25,6 +25,12 @@ rustical_store = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
chrono-tz = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
ical.workspace = true
|
||||
percent-encoding.workspace = true
|
||||
rustical_xml.workspace = true
|
||||
uuid.workspace = true
|
||||
rustical_dav_push.workspace = true
|
||||
rustical_ical.workspace = true
|
||||
http.workspace = true
|
||||
headers.workspace = true
|
||||
tower-http.workspace = true
|
||||
|
||||
103
crates/caldav/src/calendar/methods/get.rs
Normal file
103
crates/caldav/src/calendar/methods/get.rs
Normal file
@@ -0,0 +1,103 @@
|
||||
use crate::Error;
|
||||
use crate::calendar::CalendarResourceService;
|
||||
use axum::body::Body;
|
||||
use axum::extract::State;
|
||||
use axum::{extract::Path, response::Response};
|
||||
use headers::{ContentType, HeaderMapExt};
|
||||
use http::{HeaderValue, StatusCode, header};
|
||||
use ical::generator::{Emitter, IcalCalendarBuilder};
|
||||
use ical::property::Property;
|
||||
use percent_encoding::{CONTROLS, utf8_percent_encode};
|
||||
use rustical_ical::{CalendarObjectComponent, EventObject, JournalObject, TodoObject};
|
||||
use rustical_store::{CalendarStore, SubscriptionStore, auth::User};
|
||||
use std::collections::HashMap;
|
||||
use std::str::FromStr;
|
||||
use tracing::instrument;
|
||||
|
||||
#[instrument(skip(cal_store))]
|
||||
pub async fn route_get<C: CalendarStore, S: SubscriptionStore>(
|
||||
Path((principal, calendar_id)): Path<(String, String)>,
|
||||
State(CalendarResourceService { cal_store, .. }): State<CalendarResourceService<C, S>>,
|
||||
user: User,
|
||||
) -> Result<Response, Error> {
|
||||
if !user.is_principal(&principal) {
|
||||
return Err(crate::Error::Unauthorized);
|
||||
}
|
||||
|
||||
let calendar = cal_store.get_calendar(&principal, &calendar_id).await?;
|
||||
if !user.is_principal(&calendar.principal) {
|
||||
return Err(crate::Error::Unauthorized);
|
||||
}
|
||||
|
||||
let calendar = cal_store.get_calendar(&principal, &calendar_id).await?;
|
||||
|
||||
let mut timezones = HashMap::new();
|
||||
let objects = cal_store.get_objects(&principal, &calendar_id).await?;
|
||||
|
||||
let mut ical_calendar_builder = IcalCalendarBuilder::version("4.0")
|
||||
.gregorian()
|
||||
.prodid("RustiCal");
|
||||
if calendar.displayname.is_some() {
|
||||
ical_calendar_builder = ical_calendar_builder.set(Property {
|
||||
name: "X-WR-CALNAME".to_owned(),
|
||||
value: calendar.displayname,
|
||||
params: None,
|
||||
});
|
||||
}
|
||||
if calendar.description.is_some() {
|
||||
ical_calendar_builder = ical_calendar_builder.set(Property {
|
||||
name: "X-WR-CALDESC".to_owned(),
|
||||
value: calendar.description,
|
||||
params: None,
|
||||
});
|
||||
}
|
||||
if calendar.timezone_id.is_some() {
|
||||
ical_calendar_builder = ical_calendar_builder.set(Property {
|
||||
name: "X-WR-TIMEZONE".to_owned(),
|
||||
value: calendar.timezone_id,
|
||||
params: None,
|
||||
});
|
||||
}
|
||||
if calendar.color.is_some() {
|
||||
ical_calendar_builder = ical_calendar_builder.set(Property {
|
||||
name: "X-RUSTICAL-COLOR".to_owned(),
|
||||
value: calendar.color,
|
||||
params: None,
|
||||
});
|
||||
}
|
||||
let mut ical_calendar = ical_calendar_builder.build();
|
||||
|
||||
for object in &objects {
|
||||
match object.get_data() {
|
||||
CalendarObjectComponent::Event(EventObject {
|
||||
event,
|
||||
timezones: object_timezones,
|
||||
..
|
||||
}) => {
|
||||
timezones.extend(object_timezones);
|
||||
ical_calendar.events.push(event.clone());
|
||||
}
|
||||
CalendarObjectComponent::Todo(TodoObject { todo, .. }) => {
|
||||
ical_calendar.todos.push(todo.clone());
|
||||
}
|
||||
CalendarObjectComponent::Journal(JournalObject { journal, .. }) => {
|
||||
ical_calendar.journals.push(journal.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut resp = Response::builder().status(StatusCode::OK);
|
||||
let hdrs = resp.headers_mut().unwrap();
|
||||
hdrs.typed_insert(ContentType::from_str("text/calendar").unwrap());
|
||||
|
||||
let filename = format!("{}_{}.ics", calendar.principal, calendar.id);
|
||||
let filename = utf8_percent_encode(&filename, CONTROLS);
|
||||
hdrs.insert(
|
||||
header::CONTENT_DISPOSITION,
|
||||
HeaderValue::from_str(&format!(
|
||||
"attachement; filename*=UTF-8''{filename}; filename={filename}",
|
||||
))
|
||||
.unwrap(),
|
||||
);
|
||||
Ok(resp.body(Body::new(ical_calendar.generate())).unwrap())
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
use crate::Error;
|
||||
use crate::calendar::CalendarResourceService;
|
||||
use crate::calendar::prop::SupportedCalendarComponentSet;
|
||||
use actix_web::HttpResponse;
|
||||
use actix_web::web::{Data, Path};
|
||||
use axum::extract::{Path, State};
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use http::{Method, StatusCode};
|
||||
use rustical_ical::CalendarObjectType;
|
||||
use rustical_store::auth::User;
|
||||
use rustical_store::calendar::CalendarObjectType;
|
||||
use rustical_store::{Calendar, CalendarStore};
|
||||
use rustical_store::{Calendar, CalendarStore, SubscriptionStore};
|
||||
use rustical_xml::{Unparsed, XmlDeserialize, XmlDocument, XmlRootTag};
|
||||
use tracing::instrument;
|
||||
use tracing_actix_web::RootSpan;
|
||||
|
||||
#[derive(XmlDeserialize, Clone, Debug)]
|
||||
pub struct MkcolCalendarProp {
|
||||
@@ -48,21 +49,31 @@ struct MkcalendarRequest {
|
||||
set: PropElement,
|
||||
}
|
||||
|
||||
#[instrument(parent = root_span.id(), skip(store, root_span))]
|
||||
pub async fn route_mkcalendar<C: CalendarStore>(
|
||||
path: Path<(String, String)>,
|
||||
body: String,
|
||||
#[derive(XmlDeserialize, XmlRootTag, Clone, Debug)]
|
||||
#[xml(root = b"mkcol")]
|
||||
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
|
||||
struct MkcolRequest {
|
||||
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
|
||||
set: PropElement,
|
||||
}
|
||||
|
||||
#[instrument(skip(cal_store))]
|
||||
pub async fn route_mkcalendar<C: CalendarStore, S: SubscriptionStore>(
|
||||
Path((principal, cal_id)): Path<(String, String)>,
|
||||
user: User,
|
||||
store: Data<C>,
|
||||
root_span: RootSpan,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let (principal, cal_id) = path.into_inner();
|
||||
State(CalendarResourceService { cal_store, .. }): State<CalendarResourceService<C, S>>,
|
||||
method: Method,
|
||||
body: String,
|
||||
) -> Result<Response, Error> {
|
||||
if !user.is_principal(&principal) {
|
||||
return Err(Error::Unauthorized);
|
||||
}
|
||||
|
||||
let request = MkcalendarRequest::parse_str(&body)?;
|
||||
let request = request.set.prop;
|
||||
let request = match method.as_str() {
|
||||
"MKCALENDAR" => MkcalendarRequest::parse_str(&body)?.set.prop,
|
||||
"MKCOL" => MkcolRequest::parse_str(&body)?.set.prop,
|
||||
_ => unreachable!("We never call with another method"),
|
||||
};
|
||||
|
||||
let calendar = Calendar {
|
||||
id: cal_id.to_owned(),
|
||||
@@ -87,17 +98,9 @@ pub async fn route_mkcalendar<C: CalendarStore>(
|
||||
]),
|
||||
};
|
||||
|
||||
match store.insert_calendar(calendar).await {
|
||||
// The spec says we should return a mkcalendar-response but I don't know what goes into it.
|
||||
// However, it works without one but breaks on iPadOS when using an empty one :)
|
||||
Ok(()) => Ok(HttpResponse::Created()
|
||||
.insert_header(("Cache-Control", "no-cache"))
|
||||
.body("")),
|
||||
Err(err) => {
|
||||
dbg!(err.to_string());
|
||||
Err(err.into())
|
||||
}
|
||||
}
|
||||
cal_store.insert_calendar(calendar).await?;
|
||||
// The spec says we don't have to return a response everything was successful
|
||||
Ok(StatusCode::CREATED.into_response())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -130,4 +133,31 @@ mod tests {
|
||||
</CAL:mkcalendar>
|
||||
"#).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_xml_mkcol() {
|
||||
MkcolRequest::parse_str(r#"
|
||||
<?xml version='1.0' encoding='UTF-8' ?>
|
||||
<mkcol xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav">
|
||||
<set>
|
||||
<prop>
|
||||
<resourcetype>
|
||||
<collection />
|
||||
<CAL:calendar />
|
||||
</resourcetype>
|
||||
<displayname>jfs</displayname>
|
||||
<CAL:calendar-description>rggg</CAL:calendar-description>
|
||||
<n0:calendar-color xmlns:n0="http://apple.com/ns/ical/">#FFF8DCFF</n0:calendar-color>
|
||||
<CAL:calendar-timezone-id>Europe/Berlin</CAL:calendar-timezone-id>
|
||||
<CAL:supported-calendar-component-set>
|
||||
<CAL:comp name="VEVENT"/>
|
||||
<CAL:comp name="VTODO"/>
|
||||
<CAL:comp name="VJOURNAL"/>
|
||||
</CAL:supported-calendar-component-set>
|
||||
<CAL:calendar-timezone>BEGIN:VCALENDAR\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Berlin\r\nLAST-MODIFIED:20240422T053450Z\r\nTZURL:https://www.tzurl.org/zoneinfo/Europe/Berlin\r\nX-LIC-LOCATION:Europe/Berlin\r\nX-PROLEPTIC-TZNAME:LMT\r\nBEGIN:STANDARD\r\nTZNAME:CET\r\nTZOFFSETFROM:+005328\r\nTZOFFSETTO:+0100\r\nDTSTART:18930401T000632\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZNAME:CEST\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nDTSTART:19160430T230000\r\nRDATE:19400401T020000\r\nRDATE:19430329T020000\r\nRDATE:19460414T020000\r\nRDATE:19470406T030000\r\nRDATE:19480418T020000\r\nRDATE:19490410T020000\r\nRDATE:19800406T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZNAME:CET\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nDTSTART:19161001T010000\r\nRDATE:19421102T030000\r\nRDATE:19431004T030000\r\nRDATE:19441002T030000\r\nRDATE:19451118T030000\r\nRDATE:19461007T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZNAME:CEST\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nDTSTART:19170416T020000\r\nRRULE:FREQ=YEARLY;UNTIL=19180415T010000Z;BYMONTH=4;BYDAY=3MO\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZNAME:CET\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nDTSTART:19170917T030000\r\nRRULE:FREQ=YEARLY;UNTIL=19180916T010000Z;BYMONTH=9;BYDAY=3MO\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZNAME:CEST\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nDTSTART:19440403T020000\r\nRRULE:FREQ=YEARLY;UNTIL=19450402T010000Z;BYMONTH=4;BYDAY=1MO\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZNAME:CEMT\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0300\r\nDTSTART:19450524T000000\r\nRDATE:19470511T010000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZNAME:CEST\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0200\r\nDTSTART:19450924T030000\r\nRDATE:19470629T030000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZNAME:CET\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0100\r\nDTSTART:19460101T000000\r\nRDATE:19800101T000000\r\nEND:STANDARD\r\nBEGIN:STANDARD\r\nTZNAME:CET\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nDTSTART:19471005T030000\r\nRRULE:FREQ=YEARLY;UNTIL=19491002T010000Z;BYMONTH=10;BYDAY=1SU\r\nEND:STANDARD\r\nBEGIN:STANDARD\r\nTZNAME:CET\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nDTSTART:19800928T030000\r\nRRULE:FREQ=YEARLY;UNTIL=19950924T010000Z;BYMONTH=9;BYDAY=-1SU\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZNAME:CEST\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nDTSTART:19810329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZNAME:CET\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nDTSTART:19961027T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR\r\n</CAL:calendar-timezone>
|
||||
</prop>
|
||||
</set>
|
||||
</mkcol>
|
||||
"#).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
pub mod mkcalendar;
|
||||
pub mod post;
|
||||
// pub mod post;
|
||||
pub mod get;
|
||||
pub mod put;
|
||||
pub mod report;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::Error;
|
||||
use crate::calendar::resource::CalendarResource;
|
||||
use actix_web::http::header;
|
||||
use actix_web::web::{Data, Path};
|
||||
use actix_web::{HttpRequest, HttpResponse};
|
||||
use crate::calendar::resource::{CalendarResource, CalendarResourceService};
|
||||
use axum::extract::{Path, State};
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use http::{HeaderMap, StatusCode, header};
|
||||
use rustical_dav::privileges::UserPrivilege;
|
||||
use rustical_dav::resource::Resource;
|
||||
use rustical_dav_push::register::PushRegister;
|
||||
@@ -10,24 +10,22 @@ use rustical_store::auth::User;
|
||||
use rustical_store::{CalendarStore, Subscription, SubscriptionStore};
|
||||
use rustical_xml::XmlDocument;
|
||||
use tracing::instrument;
|
||||
use tracing_actix_web::RootSpan;
|
||||
|
||||
#[instrument(parent = root_span.id(), skip(store, subscription_store, root_span, req))]
|
||||
#[instrument(skip(resource_service))]
|
||||
pub async fn route_post<C: CalendarStore, S: SubscriptionStore>(
|
||||
path: Path<(String, String)>,
|
||||
body: String,
|
||||
Path((principal, cal_id)): Path<(String, String)>,
|
||||
user: User,
|
||||
store: Data<C>,
|
||||
subscription_store: Data<S>,
|
||||
root_span: RootSpan,
|
||||
req: HttpRequest,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let (principal, cal_id) = path.into_inner();
|
||||
State(resource_service): State<CalendarResourceService<C, S>>,
|
||||
body: String,
|
||||
) -> Result<Response, Error> {
|
||||
if !user.is_principal(&principal) {
|
||||
return Err(Error::Unauthorized);
|
||||
}
|
||||
|
||||
let calendar = store.get_calendar(&principal, &cal_id).await?;
|
||||
let calendar = resource_service
|
||||
.cal_store
|
||||
.get_calendar(&principal, &cal_id)
|
||||
.await?;
|
||||
let calendar_resource = CalendarResource {
|
||||
cal: calendar,
|
||||
read_only: true,
|
||||
@@ -70,12 +68,22 @@ pub async fn route_post<C: CalendarStore, S: SubscriptionStore>(
|
||||
.ty,
|
||||
auth_secret: request.subscription.web_push_subscription.auth_secret,
|
||||
};
|
||||
subscription_store.upsert_subscription(subscription).await?;
|
||||
resource_service
|
||||
.sub_store
|
||||
.upsert_subscription(subscription)
|
||||
.await?;
|
||||
|
||||
let location = req
|
||||
.resource_map()
|
||||
.url_for(&req, "subscription", &[sub_id])
|
||||
.unwrap();
|
||||
// let location = req
|
||||
// .resource_map()
|
||||
// .url_for(&req, "subscription", &[sub_id])
|
||||
// .unwrap();
|
||||
//
|
||||
let location = "asd";
|
||||
Ok((
|
||||
StatusCode::CREATED,
|
||||
HeaderMap::from_iter([(header::LOCATION, location)]),
|
||||
)
|
||||
.into_response());
|
||||
|
||||
Ok(HttpResponse::Created()
|
||||
.append_header((header::LOCATION, location.to_string()))
|
||||
|
||||
101
crates/caldav/src/calendar/methods/put.rs
Normal file
101
crates/caldav/src/calendar/methods/put.rs
Normal file
@@ -0,0 +1,101 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::calendar::prop::SupportedCalendarComponent;
|
||||
use crate::calendar::{self, CalendarResourceService};
|
||||
use crate::{Error, calendar_set};
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use http::StatusCode;
|
||||
use ical::generator::Emitter;
|
||||
use ical::parser::ical::component::IcalTimeZone;
|
||||
use ical::{IcalParser, parser::Component};
|
||||
use rustical_ical::CalendarObjectType;
|
||||
use rustical_store::{Calendar, CalendarStore, SubscriptionStore, auth::User};
|
||||
use tracing::instrument;
|
||||
|
||||
#[instrument(skip(cal_store))]
|
||||
pub async fn route_put<C: CalendarStore, S: SubscriptionStore>(
|
||||
Path((principal, cal_id)): Path<(String, String)>,
|
||||
State(CalendarResourceService { cal_store, .. }): State<CalendarResourceService<C, S>>,
|
||||
user: User,
|
||||
body: String,
|
||||
) -> Result<Response, Error> {
|
||||
if !user.is_principal(&principal) {
|
||||
return Err(crate::Error::Unauthorized);
|
||||
}
|
||||
|
||||
let mut parser = IcalParser::new(body.as_bytes());
|
||||
let cal = parser
|
||||
.next()
|
||||
.ok_or(rustical_ical::Error::MissingCalendar)?
|
||||
.map_err(rustical_ical::Error::from)?;
|
||||
if parser.next().is_some() {
|
||||
return Err(rustical_ical::Error::InvalidData(
|
||||
"multiple calendars, only one allowed".to_owned(),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
if !cal.alarms.is_empty() || !cal.free_busys.is_empty() {
|
||||
return Err(rustical_ical::Error::InvalidData(
|
||||
"Importer does not support VALARM and VFREEBUSY components".to_owned(),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
let mut objects = vec![];
|
||||
for event in cal.events {}
|
||||
for todo in cal.todos {}
|
||||
for journal in cal.journals {}
|
||||
|
||||
let timezones: HashMap<String, IcalTimeZone> = cal
|
||||
.timezones
|
||||
.clone()
|
||||
.into_iter()
|
||||
.filter_map(|timezone| {
|
||||
let timezone_prop = timezone.get_property("TZID")?.to_owned();
|
||||
let tzid = timezone_prop.value?;
|
||||
Some((tzid, timezone))
|
||||
})
|
||||
.collect();
|
||||
|
||||
let displayname = cal.get_property("X-WR-CALNAME").and_then(|prop| prop.value);
|
||||
let description = cal.get_property("X-WR-CALDESC").and_then(|prop| prop.value);
|
||||
let color = cal
|
||||
.get_property("X-RUSTICAL-COLOR")
|
||||
.and_then(|prop| prop.value);
|
||||
let timezone_id = cal
|
||||
.get_property("X-WR-TIMEZONE")
|
||||
.and_then(|prop| prop.value);
|
||||
let timezone = timezone_id
|
||||
.and_then(|tzid| timezones.get(&tzid))
|
||||
.map(|timezone| timezone.generate());
|
||||
|
||||
let mut components = vec![CalendarObjectType::Event, CalendarObjectType::Todo];
|
||||
if !cal.journals.is_empty() {
|
||||
components.push(CalendarObjectType::Journal);
|
||||
}
|
||||
|
||||
let calendar = Calendar {
|
||||
principal: principal.clone(),
|
||||
id: cal_id,
|
||||
displayname,
|
||||
description,
|
||||
color,
|
||||
timezone_id,
|
||||
timezone,
|
||||
components,
|
||||
subscription_url: None,
|
||||
push_topic: uuid::Uuid::new_v4().to_string(),
|
||||
synctoken: 0,
|
||||
deleted_at: None,
|
||||
order: 0,
|
||||
};
|
||||
|
||||
cal_store
|
||||
.import_calendar(&principal, calendar, objects)
|
||||
.await?;
|
||||
|
||||
Ok(StatusCode::CREATED.into_response())
|
||||
}
|
||||
@@ -1,18 +1,7 @@
|
||||
use super::ReportPropName;
|
||||
use crate::{
|
||||
Error,
|
||||
calendar_object::resource::{CalendarObjectPropWrapper, CalendarObjectResource},
|
||||
};
|
||||
use actix_web::{
|
||||
HttpRequest,
|
||||
dev::{Path, ResourceDef},
|
||||
http::StatusCode,
|
||||
};
|
||||
use rustical_dav::{
|
||||
resource::Resource,
|
||||
xml::{MultistatusElement, PropfindType, multistatus::ResponseElement},
|
||||
};
|
||||
use rustical_store::{CalendarObject, CalendarStore, auth::User};
|
||||
use crate::{Error, calendar_object::CalendarObjectPropWrapperName};
|
||||
use rustical_dav::xml::PropfindType;
|
||||
use rustical_ical::CalendarObject;
|
||||
use rustical_store::CalendarStore;
|
||||
use rustical_xml::XmlDeserialize;
|
||||
|
||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
|
||||
@@ -20,7 +9,7 @@ use rustical_xml::XmlDeserialize;
|
||||
// <!ELEMENT calendar-query ((DAV:allprop | DAV:propname | DAV:prop)?, href+)>
|
||||
pub(crate) struct CalendarMultigetRequest {
|
||||
#[xml(ty = "untagged")]
|
||||
pub(crate) prop: PropfindType<ReportPropName>,
|
||||
pub(crate) prop: PropfindType<CalendarObjectPropWrapperName>,
|
||||
#[xml(flatten)]
|
||||
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
|
||||
pub(crate) href: Vec<String>,
|
||||
@@ -33,65 +22,27 @@ pub async fn get_objects_calendar_multiget<C: CalendarStore>(
|
||||
cal_id: &str,
|
||||
store: &C,
|
||||
) -> Result<(Vec<CalendarObject>, Vec<String>), Error> {
|
||||
let resource_def = ResourceDef::prefix(path).join(&ResourceDef::new("/{object_id}.ics"));
|
||||
|
||||
let mut result = vec![];
|
||||
let mut not_found = vec![];
|
||||
|
||||
for href in &cal_query.href {
|
||||
let mut path = Path::new(href.as_str());
|
||||
if !resource_def.capture_match_info(&mut path) {
|
||||
if let Some(filename) = href.strip_prefix(path) {
|
||||
let filename = filename.trim_start_matches("/");
|
||||
if let Some(object_id) = filename.strip_suffix(".ics") {
|
||||
match store.get_object(principal, cal_id, object_id).await {
|
||||
Ok(object) => result.push(object),
|
||||
Err(rustical_store::Error::NotFound) => not_found.push(href.to_owned()),
|
||||
Err(err) => return Err(err.into()),
|
||||
};
|
||||
} else {
|
||||
not_found.push(href.to_owned());
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
not_found.push(href.to_owned());
|
||||
continue;
|
||||
};
|
||||
let object_id = path.get("object_id").unwrap();
|
||||
match store.get_object(principal, cal_id, object_id).await {
|
||||
Ok(object) => result.push(object),
|
||||
Err(rustical_store::Error::NotFound) => not_found.push(href.to_owned()),
|
||||
Err(err) => return Err(err.into()),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Ok((result, not_found))
|
||||
}
|
||||
|
||||
pub async fn handle_calendar_multiget<C: CalendarStore>(
|
||||
cal_multiget: &CalendarMultigetRequest,
|
||||
props: &[&str],
|
||||
req: HttpRequest,
|
||||
user: &User,
|
||||
principal: &str,
|
||||
cal_id: &str,
|
||||
cal_store: &C,
|
||||
) -> Result<MultistatusElement<CalendarObjectPropWrapper, String>, Error> {
|
||||
let (objects, not_found) =
|
||||
get_objects_calendar_multiget(cal_multiget, req.path(), principal, cal_id, cal_store)
|
||||
.await?;
|
||||
|
||||
let mut responses = Vec::new();
|
||||
for object in objects {
|
||||
let path = format!("{}/{}.ics", req.path(), object.get_id());
|
||||
responses.push(
|
||||
CalendarObjectResource {
|
||||
object,
|
||||
principal: principal.to_owned(),
|
||||
}
|
||||
.propfind(&path, props, user, req.resource_map())?,
|
||||
);
|
||||
}
|
||||
|
||||
let not_found_responses = not_found
|
||||
.into_iter()
|
||||
.map(|path| ResponseElement {
|
||||
href: path,
|
||||
status: Some(StatusCode::NOT_FOUND),
|
||||
..Default::default()
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(MultistatusElement {
|
||||
responses,
|
||||
member_responses: not_found_responses,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,21 +1,10 @@
|
||||
use actix_web::HttpRequest;
|
||||
use rustical_dav::{
|
||||
resource::Resource,
|
||||
xml::{MultistatusElement, PropfindType},
|
||||
};
|
||||
use rustical_store::{
|
||||
CalendarObject, CalendarStore, auth::User, calendar::UtcDateTime, calendar_store::CalendarQuery,
|
||||
};
|
||||
use crate::{Error, calendar_object::CalendarObjectPropWrapperName};
|
||||
use rustical_dav::xml::PropfindType;
|
||||
use rustical_ical::{CalendarObject, UtcDateTime};
|
||||
use rustical_store::{CalendarStore, calendar_store::CalendarQuery};
|
||||
use rustical_xml::XmlDeserialize;
|
||||
use std::ops::Deref;
|
||||
|
||||
use crate::{
|
||||
Error,
|
||||
calendar_object::resource::{CalendarObjectPropWrapper, CalendarObjectResource},
|
||||
};
|
||||
|
||||
use super::ReportPropName;
|
||||
|
||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
|
||||
#[allow(dead_code)]
|
||||
pub(crate) struct TimeRangeElement {
|
||||
@@ -181,7 +170,7 @@ impl From<&FilterElement> for CalendarQuery {
|
||||
// <!ELEMENT calendar-query ((DAV:allprop | DAV:propname | DAV:prop)?, filter, timezone?)>
|
||||
pub struct CalendarQueryRequest {
|
||||
#[xml(ty = "untagged")]
|
||||
pub prop: PropfindType<ReportPropName>,
|
||||
pub prop: PropfindType<CalendarObjectPropWrapperName>,
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
||||
pub(crate) filter: Option<FilterElement>,
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
||||
@@ -214,36 +203,3 @@ pub async fn get_objects_calendar_query<C: CalendarStore>(
|
||||
}
|
||||
Ok(objects)
|
||||
}
|
||||
|
||||
pub async fn handle_calendar_query<C: CalendarStore>(
|
||||
cal_query: &CalendarQueryRequest,
|
||||
props: &[&str],
|
||||
req: HttpRequest,
|
||||
user: &User,
|
||||
principal: &str,
|
||||
cal_id: &str,
|
||||
cal_store: &C,
|
||||
) -> Result<MultistatusElement<CalendarObjectPropWrapper, String>, Error> {
|
||||
let objects = get_objects_calendar_query(cal_query, principal, cal_id, cal_store).await?;
|
||||
|
||||
let mut responses = Vec::new();
|
||||
for object in objects {
|
||||
let path = format!(
|
||||
"{}/{}.ics",
|
||||
req.path().trim_end_matches('/'),
|
||||
object.get_id()
|
||||
);
|
||||
responses.push(
|
||||
CalendarObjectResource {
|
||||
object,
|
||||
principal: principal.to_owned(),
|
||||
}
|
||||
.propfind(&path, props, user, req.resource_map())?,
|
||||
);
|
||||
}
|
||||
|
||||
Ok(MultistatusElement {
|
||||
responses,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,14 +1,27 @@
|
||||
use crate::Error;
|
||||
use actix_web::{
|
||||
HttpRequest, Responder,
|
||||
web::{Data, Path},
|
||||
use crate::{
|
||||
CalDavPrincipalUri, Error,
|
||||
calendar::CalendarResourceService,
|
||||
calendar_object::{
|
||||
CalendarObjectPropWrapper, CalendarObjectPropWrapperName, resource::CalendarObjectResource,
|
||||
},
|
||||
};
|
||||
use calendar_multiget::{CalendarMultigetRequest, handle_calendar_multiget};
|
||||
use calendar_query::{CalendarQueryRequest, handle_calendar_query};
|
||||
use rustical_dav::xml::{
|
||||
PropElement, PropfindType, Propname, sync_collection::SyncCollectionRequest,
|
||||
use axum::{
|
||||
Extension,
|
||||
extract::{OriginalUri, Path, State},
|
||||
response::IntoResponse,
|
||||
};
|
||||
use rustical_store::{CalendarStore, auth::User};
|
||||
use calendar_multiget::{CalendarMultigetRequest, get_objects_calendar_multiget};
|
||||
use calendar_query::{CalendarQueryRequest, get_objects_calendar_query};
|
||||
use http::StatusCode;
|
||||
use rustical_dav::{
|
||||
resource::{PrincipalUri, Resource},
|
||||
xml::{
|
||||
MultistatusElement, PropfindType, multistatus::ResponseElement,
|
||||
sync_collection::SyncCollectionRequest,
|
||||
},
|
||||
};
|
||||
use rustical_ical::CalendarObject;
|
||||
use rustical_store::{CalendarStore, SubscriptionStore, auth::User};
|
||||
use rustical_xml::{XmlDeserialize, XmlDocument};
|
||||
use sync_collection::handle_sync_collection;
|
||||
use tracing::instrument;
|
||||
@@ -17,34 +30,6 @@ mod calendar_multiget;
|
||||
mod calendar_query;
|
||||
mod sync_collection;
|
||||
|
||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
|
||||
pub(crate) struct ExpandElement {
|
||||
#[xml(ty = "attr")]
|
||||
start: String,
|
||||
#[xml(ty = "attr")]
|
||||
end: String,
|
||||
}
|
||||
|
||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
|
||||
pub struct CalendarData {
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
||||
comp: Option<()>,
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
||||
expand: Option<ExpandElement>,
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
||||
limit_recurrence_set: Option<()>,
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
||||
limit_freebusy_set: Option<()>,
|
||||
}
|
||||
|
||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
|
||||
pub enum ReportPropName {
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
||||
CalendarData(CalendarData),
|
||||
#[xml(other)]
|
||||
Propname(Propname),
|
||||
}
|
||||
|
||||
#[derive(XmlDeserialize, XmlDocument, Clone, Debug, PartialEq)]
|
||||
pub(crate) enum ReportRequest {
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
||||
@@ -52,44 +37,65 @@ pub(crate) enum ReportRequest {
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
||||
CalendarQuery(CalendarQueryRequest),
|
||||
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
|
||||
SyncCollection(SyncCollectionRequest<ReportPropName>),
|
||||
SyncCollection(SyncCollectionRequest<CalendarObjectPropWrapperName>),
|
||||
}
|
||||
|
||||
impl ReportRequest {
|
||||
fn props(&self) -> Vec<&str> {
|
||||
let prop_element = match self {
|
||||
fn props(&self) -> &PropfindType<CalendarObjectPropWrapperName> {
|
||||
match &self {
|
||||
ReportRequest::CalendarMultiget(CalendarMultigetRequest { prop, .. }) => prop,
|
||||
ReportRequest::CalendarQuery(CalendarQueryRequest { prop, .. }) => prop,
|
||||
ReportRequest::SyncCollection(SyncCollectionRequest { prop, .. }) => prop,
|
||||
};
|
||||
|
||||
match prop_element {
|
||||
PropfindType::Allprop => {
|
||||
vec!["allprop"]
|
||||
}
|
||||
PropfindType::Propname => {
|
||||
vec!["propname"]
|
||||
}
|
||||
PropfindType::Prop(PropElement(prop_tags)) => prop_tags
|
||||
.iter()
|
||||
.map(|propname| match propname {
|
||||
ReportPropName::Propname(propname) => propname.0.as_str(),
|
||||
ReportPropName::CalendarData(_) => "calendar-data",
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(req, cal_store))]
|
||||
pub async fn route_report_calendar<C: CalendarStore>(
|
||||
path: Path<(String, String)>,
|
||||
body: String,
|
||||
fn objects_response(
|
||||
objects: Vec<CalendarObject>,
|
||||
not_found: Vec<String>,
|
||||
path: &str,
|
||||
principal: &str,
|
||||
puri: &impl PrincipalUri,
|
||||
user: &User,
|
||||
prop: &PropfindType<CalendarObjectPropWrapperName>,
|
||||
) -> Result<MultistatusElement<CalendarObjectPropWrapper, String>, Error> {
|
||||
let mut responses = Vec::new();
|
||||
for object in objects {
|
||||
let path = format!("{}/{}.ics", path, object.get_id());
|
||||
responses.push(
|
||||
CalendarObjectResource {
|
||||
object,
|
||||
principal: principal.to_owned(),
|
||||
}
|
||||
.propfind(&path, prop, puri, user)?,
|
||||
);
|
||||
}
|
||||
|
||||
let not_found_responses = not_found
|
||||
.into_iter()
|
||||
.map(|path| ResponseElement {
|
||||
href: path,
|
||||
status: Some(StatusCode::NOT_FOUND),
|
||||
..Default::default()
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(MultistatusElement {
|
||||
responses,
|
||||
member_responses: not_found_responses,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(cal_store))]
|
||||
pub async fn route_report_calendar<C: CalendarStore, S: SubscriptionStore>(
|
||||
Path((principal, cal_id)): Path<(String, String)>,
|
||||
user: User,
|
||||
req: HttpRequest,
|
||||
cal_store: Data<C>,
|
||||
) -> Result<impl Responder, Error> {
|
||||
let (principal, cal_id) = path.into_inner();
|
||||
Extension(puri): Extension<CalDavPrincipalUri>,
|
||||
State(CalendarResourceService { cal_store, .. }): State<CalendarResourceService<C, S>>,
|
||||
OriginalUri(uri): OriginalUri,
|
||||
body: String,
|
||||
) -> Result<impl IntoResponse, Error> {
|
||||
if !user.is_principal(&principal) {
|
||||
return Err(Error::Unauthorized);
|
||||
}
|
||||
@@ -99,34 +105,35 @@ pub async fn route_report_calendar<C: CalendarStore>(
|
||||
|
||||
Ok(match &request {
|
||||
ReportRequest::CalendarQuery(cal_query) => {
|
||||
handle_calendar_query(
|
||||
cal_query,
|
||||
&props,
|
||||
req,
|
||||
&user,
|
||||
&principal,
|
||||
&cal_id,
|
||||
cal_store.as_ref(),
|
||||
)
|
||||
.await?
|
||||
let objects =
|
||||
get_objects_calendar_query(cal_query, &principal, &cal_id, cal_store.as_ref())
|
||||
.await?;
|
||||
objects_response(objects, vec![], uri.path(), &principal, &puri, &user, props)?
|
||||
}
|
||||
ReportRequest::CalendarMultiget(cal_multiget) => {
|
||||
handle_calendar_multiget(
|
||||
let (objects, not_found) = get_objects_calendar_multiget(
|
||||
cal_multiget,
|
||||
&props,
|
||||
req,
|
||||
&user,
|
||||
uri.path(),
|
||||
&principal,
|
||||
&cal_id,
|
||||
cal_store.as_ref(),
|
||||
)
|
||||
.await?
|
||||
.await?;
|
||||
objects_response(
|
||||
objects,
|
||||
not_found,
|
||||
uri.path(),
|
||||
&principal,
|
||||
&puri,
|
||||
&user,
|
||||
props,
|
||||
)?
|
||||
}
|
||||
ReportRequest::SyncCollection(sync_collection) => {
|
||||
handle_sync_collection(
|
||||
sync_collection,
|
||||
&props,
|
||||
req,
|
||||
uri.path(),
|
||||
&puri,
|
||||
&user,
|
||||
&principal,
|
||||
&cal_id,
|
||||
@@ -140,10 +147,11 @@ pub async fn route_report_calendar<C: CalendarStore>(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::calendar_object::{CalendarData, CalendarObjectPropName, ExpandElement};
|
||||
use calendar_query::{CompFilterElement, FilterElement, TimeRangeElement};
|
||||
use rustical_dav::xml::{PropElement, PropfindType, Propname};
|
||||
use rustical_store::calendar::UtcDateTime;
|
||||
use rustical_xml::ValueDeserialize;
|
||||
use rustical_dav::xml::PropElement;
|
||||
use rustical_ical::UtcDateTime;
|
||||
use rustical_xml::{NamespaceOwned, ValueDeserialize};
|
||||
|
||||
#[test]
|
||||
fn test_xml_calendar_data() {
|
||||
@@ -165,10 +173,14 @@ mod tests {
|
||||
report_request,
|
||||
ReportRequest::CalendarMultiget(CalendarMultigetRequest {
|
||||
prop: rustical_dav::xml::PropfindType::Prop(PropElement(vec![
|
||||
ReportPropName::Propname(Propname("getetag".to_owned())),
|
||||
ReportPropName::Propname(Propname("displayname".to_owned())),
|
||||
ReportPropName::CalendarData(CalendarData { comp: None, expand: Some(ExpandElement { start: "20250426T220000Z".to_owned(), end: "20250503T220000Z".to_owned() }), limit_recurrence_set: None, limit_freebusy_set: None })
|
||||
])),
|
||||
CalendarObjectPropWrapperName::CalendarObject(CalendarObjectPropName::Getetag),
|
||||
CalendarObjectPropWrapperName::CalendarObject(CalendarObjectPropName::CalendarData(
|
||||
CalendarData { comp: None, expand: Some(ExpandElement {
|
||||
start: <UtcDateTime as ValueDeserialize>::deserialize("20250426T220000Z").unwrap(),
|
||||
end: <UtcDateTime as ValueDeserialize>::deserialize("20250503T220000Z").unwrap(),
|
||||
}), limit_recurrence_set: None, limit_freebusy_set: None }
|
||||
)),
|
||||
], vec![(Some(NamespaceOwned(Vec::from("DAV:"))), "displayname".to_string())])),
|
||||
href: vec![
|
||||
"/caldav/user/user/6f787542-5256-401a-8db97003260da/ae7a998fdfd1d84a20391168962c62b".to_owned()
|
||||
]
|
||||
@@ -198,9 +210,12 @@ mod tests {
|
||||
assert_eq!(
|
||||
report_request,
|
||||
ReportRequest::CalendarQuery(CalendarQueryRequest {
|
||||
prop: PropfindType::Prop(PropElement(vec![ReportPropName::Propname(Propname(
|
||||
"getetag".to_owned()
|
||||
))])),
|
||||
prop: rustical_dav::xml::PropfindType::Prop(PropElement(
|
||||
vec![CalendarObjectPropWrapperName::CalendarObject(
|
||||
CalendarObjectPropName::Getetag
|
||||
),],
|
||||
vec![]
|
||||
)),
|
||||
filter: Some(FilterElement {
|
||||
comp_filter: CompFilterElement {
|
||||
is_not_defined: None,
|
||||
@@ -247,9 +262,8 @@ mod tests {
|
||||
report_request,
|
||||
ReportRequest::CalendarMultiget(CalendarMultigetRequest {
|
||||
prop: rustical_dav::xml::PropfindType::Prop(PropElement(vec![
|
||||
ReportPropName::Propname(Propname("getetag".to_owned())),
|
||||
ReportPropName::Propname(Propname("displayname".to_owned()))
|
||||
])),
|
||||
CalendarObjectPropWrapperName::CalendarObject(CalendarObjectPropName::Getetag),
|
||||
], vec![(Some(NamespaceOwned(Vec::from("DAV:"))), "displayname".to_string())])),
|
||||
href: vec![
|
||||
"/caldav/user/user/6f787542-5256-401a-8db97003260da/ae7a998fdfd1d84a20391168962c62b".to_owned()
|
||||
]
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
use super::ReportPropName;
|
||||
use crate::{
|
||||
Error,
|
||||
calendar_object::resource::{CalendarObjectPropWrapper, CalendarObjectResource},
|
||||
calendar_object::{
|
||||
CalendarObjectPropWrapper, CalendarObjectPropWrapperName, resource::CalendarObjectResource,
|
||||
},
|
||||
};
|
||||
use actix_web::{HttpRequest, http::StatusCode};
|
||||
use http::StatusCode;
|
||||
use rustical_dav::{
|
||||
resource::Resource,
|
||||
resource::{PrincipalUri, Resource},
|
||||
xml::{
|
||||
MultistatusElement, multistatus::ResponseElement, sync_collection::SyncCollectionRequest,
|
||||
},
|
||||
@@ -17,9 +18,9 @@ use rustical_store::{
|
||||
};
|
||||
|
||||
pub async fn handle_sync_collection<C: CalendarStore>(
|
||||
sync_collection: &SyncCollectionRequest<ReportPropName>,
|
||||
props: &[&str],
|
||||
req: HttpRequest,
|
||||
sync_collection: &SyncCollectionRequest<CalendarObjectPropWrapperName>,
|
||||
path: &str,
|
||||
puri: &impl PrincipalUri,
|
||||
user: &User,
|
||||
principal: &str,
|
||||
cal_id: &str,
|
||||
@@ -32,22 +33,18 @@ pub async fn handle_sync_collection<C: CalendarStore>(
|
||||
|
||||
let mut responses = Vec::new();
|
||||
for object in new_objects {
|
||||
let path = format!(
|
||||
"{}/{}.ics",
|
||||
req.path().trim_end_matches('/'),
|
||||
object.get_id()
|
||||
);
|
||||
let path = format!("{}/{}.ics", path, object.get_id());
|
||||
responses.push(
|
||||
CalendarObjectResource {
|
||||
object,
|
||||
principal: principal.to_owned(),
|
||||
}
|
||||
.propfind(&path, props, user, req.resource_map())?,
|
||||
.propfind(&path, &sync_collection.prop, puri, user)?,
|
||||
);
|
||||
}
|
||||
|
||||
for object_id in deleted_objects {
|
||||
let path = format!("{}/{}.ics", req.path().trim_end_matches('/'), object_id);
|
||||
let path = format!("{path}/{object_id}.ics");
|
||||
responses.push(ResponseElement {
|
||||
href: path,
|
||||
status: Some(StatusCode::NOT_FOUND),
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
pub mod methods;
|
||||
pub mod prop;
|
||||
pub mod resource;
|
||||
mod service;
|
||||
|
||||
pub use service::CalendarResourceService;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use derive_more::derive::{From, Into};
|
||||
use rustical_store::calendar::CalendarObjectType;
|
||||
use rustical_ical::CalendarObjectType;
|
||||
use rustical_xml::{XmlDeserialize, XmlSerialize};
|
||||
|
||||
#[derive(Debug, Clone, XmlSerialize, XmlDeserialize, PartialEq, From, Into)]
|
||||
|
||||
@@ -1,33 +1,22 @@
|
||||
use super::methods::mkcalendar::route_mkcalendar;
|
||||
use super::methods::post::route_post;
|
||||
use super::methods::report::route_report_calendar;
|
||||
use super::prop::{SupportedCalendarComponentSet, SupportedCalendarData, SupportedReportSet};
|
||||
use crate::Error;
|
||||
use crate::calendar_object::resource::CalendarObjectResource;
|
||||
use crate::principal::PrincipalResource;
|
||||
use actix_web::dev::ResourceMap;
|
||||
use actix_web::http::Method;
|
||||
use actix_web::web;
|
||||
use async_trait::async_trait;
|
||||
use chrono::{DateTime, Utc};
|
||||
use derive_more::derive::{From, Into};
|
||||
use rustical_dav::extensions::{
|
||||
CommonPropertiesExtension, CommonPropertiesProp, SyncTokenExtension, SyncTokenExtensionProp,
|
||||
};
|
||||
use rustical_dav::privileges::UserPrivilegeSet;
|
||||
use rustical_dav::resource::{Resource, ResourceService};
|
||||
use rustical_dav::resource::{PrincipalUri, Resource, ResourceName};
|
||||
use rustical_dav::xml::{HrefElement, Resourcetype, ResourcetypeInner};
|
||||
use rustical_dav_push::{DavPushExtension, DavPushExtensionProp};
|
||||
use rustical_dav_push::DavPushExtension;
|
||||
use rustical_ical::CalDateTime;
|
||||
use rustical_store::Calendar;
|
||||
use rustical_store::auth::User;
|
||||
use rustical_store::calendar::CalDateTime;
|
||||
use rustical_store::{Calendar, CalendarStore, SubscriptionStore};
|
||||
use rustical_xml::{EnumUnitVariants, EnumVariants};
|
||||
use rustical_xml::{EnumVariants, PropName};
|
||||
use rustical_xml::{XmlDeserialize, XmlSerialize};
|
||||
use std::marker::PhantomData;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, EnumUnitVariants)]
|
||||
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, PropName)]
|
||||
#[xml(unit_variants_ident = "CalendarPropName")]
|
||||
pub enum CalendarProp {
|
||||
// WebDAV (RFC 2518)
|
||||
@@ -67,12 +56,12 @@ pub enum CalendarProp {
|
||||
MaxDateTime(String),
|
||||
}
|
||||
|
||||
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, EnumUnitVariants)]
|
||||
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, PropName)]
|
||||
#[xml(unit_variants_ident = "CalendarPropWrapperName", untagged)]
|
||||
pub enum CalendarPropWrapper {
|
||||
Calendar(CalendarProp),
|
||||
SyncToken(SyncTokenExtensionProp),
|
||||
DavPush(DavPushExtensionProp),
|
||||
// DavPush(DavPushExtensionProp),
|
||||
Common(CommonPropertiesProp),
|
||||
}
|
||||
|
||||
@@ -82,6 +71,12 @@ pub struct CalendarResource {
|
||||
pub read_only: bool,
|
||||
}
|
||||
|
||||
impl ResourceName for CalendarResource {
|
||||
fn get_name(&self) -> String {
|
||||
self.cal.id.to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CalendarResource> for Calendar {
|
||||
fn from(value: CalendarResource) -> Self {
|
||||
value.cal
|
||||
@@ -100,15 +95,13 @@ impl DavPushExtension for CalendarResource {
|
||||
}
|
||||
}
|
||||
|
||||
impl CommonPropertiesExtension for CalendarResource {
|
||||
type PrincipalResource = PrincipalResource;
|
||||
}
|
||||
|
||||
impl Resource for CalendarResource {
|
||||
type Prop = CalendarPropWrapper;
|
||||
type Error = Error;
|
||||
type Principal = User;
|
||||
|
||||
const IS_COLLECTION: bool = true;
|
||||
|
||||
fn get_resourcetype(&self) -> Resourcetype {
|
||||
if self.cal.subscription_url.is_none() {
|
||||
Resourcetype(&[
|
||||
@@ -128,7 +121,7 @@ impl Resource for CalendarResource {
|
||||
|
||||
fn get_prop(
|
||||
&self,
|
||||
rmap: &ResourceMap,
|
||||
puri: &impl PrincipalUri,
|
||||
user: &User,
|
||||
prop: &CalendarPropWrapperName,
|
||||
) -> Result<Self::Prop, Self::Error> {
|
||||
@@ -170,20 +163,20 @@ impl Resource for CalendarResource {
|
||||
self.cal.subscription_url.to_owned().map(HrefElement::from),
|
||||
),
|
||||
CalendarPropName::MinDateTime => {
|
||||
CalendarProp::MinDateTime(CalDateTime::Utc(DateTime::<Utc>::MIN_UTC).format())
|
||||
CalendarProp::MinDateTime(CalDateTime::from(DateTime::<Utc>::MIN_UTC).format())
|
||||
}
|
||||
CalendarPropName::MaxDateTime => {
|
||||
CalendarProp::MaxDateTime(CalDateTime::Utc(DateTime::<Utc>::MAX_UTC).format())
|
||||
CalendarProp::MaxDateTime(CalDateTime::from(DateTime::<Utc>::MAX_UTC).format())
|
||||
}
|
||||
}),
|
||||
CalendarPropWrapperName::SyncToken(prop) => {
|
||||
CalendarPropWrapper::SyncToken(SyncTokenExtension::get_prop(self, prop)?)
|
||||
}
|
||||
CalendarPropWrapperName::DavPush(prop) => {
|
||||
CalendarPropWrapper::DavPush(DavPushExtension::get_prop(self, prop)?)
|
||||
}
|
||||
// CalendarPropWrapperName::DavPush(prop) => {
|
||||
// CalendarPropWrapper::DavPush(DavPushExtension::get_prop(self, prop)?)
|
||||
// }
|
||||
CalendarPropWrapperName::Common(prop) => CalendarPropWrapper::Common(
|
||||
CommonPropertiesExtension::get_prop(self, rmap, user, prop)?,
|
||||
CommonPropertiesExtension::get_prop(self, puri, user, prop)?,
|
||||
),
|
||||
})
|
||||
}
|
||||
@@ -243,7 +236,7 @@ impl Resource for CalendarResource {
|
||||
CalendarProp::MaxDateTime(_) => Err(rustical_dav::Error::PropReadOnly),
|
||||
},
|
||||
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),
|
||||
}
|
||||
}
|
||||
@@ -291,7 +284,7 @@ impl Resource for CalendarResource {
|
||||
CalendarPropName::MaxDateTime => Err(rustical_dav::Error::PropReadOnly),
|
||||
},
|
||||
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) => {
|
||||
CommonPropertiesExtension::remove_prop(self, prop)
|
||||
}
|
||||
@@ -314,90 +307,3 @@ impl Resource for CalendarResource {
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CalendarResourceService<C: CalendarStore, S: SubscriptionStore> {
|
||||
cal_store: Arc<C>,
|
||||
__phantom_sub: PhantomData<S>,
|
||||
}
|
||||
|
||||
impl<C: CalendarStore, S: SubscriptionStore> CalendarResourceService<C, S> {
|
||||
pub fn new(cal_store: Arc<C>) -> Self {
|
||||
Self {
|
||||
cal_store,
|
||||
__phantom_sub: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl<C: CalendarStore, S: SubscriptionStore> ResourceService for CalendarResourceService<C, S> {
|
||||
type MemberType = CalendarObjectResource;
|
||||
type PathComponents = (String, String); // principal, calendar_id
|
||||
type Resource = CalendarResource;
|
||||
type Error = Error;
|
||||
type Principal = User;
|
||||
|
||||
async fn get_resource(
|
||||
&self,
|
||||
(principal, cal_id): &Self::PathComponents,
|
||||
) -> Result<Self::Resource, Error> {
|
||||
let calendar = self.cal_store.get_calendar(principal, cal_id).await?;
|
||||
Ok(CalendarResource {
|
||||
cal: calendar,
|
||||
read_only: self.cal_store.is_read_only(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_members(
|
||||
&self,
|
||||
(principal, cal_id): &Self::PathComponents,
|
||||
) -> Result<Vec<(String, Self::MemberType)>, Self::Error> {
|
||||
Ok(self
|
||||
.cal_store
|
||||
.get_objects(principal, cal_id)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|object| {
|
||||
(
|
||||
format!("{}.ics", object.get_id()),
|
||||
CalendarObjectResource {
|
||||
object,
|
||||
principal: principal.to_owned(),
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn save_resource(
|
||||
&self,
|
||||
(principal, cal_id): &Self::PathComponents,
|
||||
file: Self::Resource,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.cal_store
|
||||
.update_calendar(principal.to_owned(), cal_id.to_owned(), file.into())
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete_resource(
|
||||
&self,
|
||||
(principal, cal_id): &Self::PathComponents,
|
||||
use_trashbin: bool,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.cal_store
|
||||
.delete_calendar(principal, cal_id, use_trashbin)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn actix_additional_routes(res: actix_web::Resource) -> actix_web::Resource {
|
||||
let report_method = web::method(Method::from_str("REPORT").unwrap());
|
||||
let mkcalendar_method = web::method(Method::from_str("MKCALENDAR").unwrap());
|
||||
|
||||
res.route(report_method.to(route_report_calendar::<C>))
|
||||
.route(mkcalendar_method.to(route_mkcalendar::<C>))
|
||||
.post(route_post::<C, S>)
|
||||
}
|
||||
}
|
||||
|
||||
143
crates/caldav/src/calendar/service.rs
Normal file
143
crates/caldav/src/calendar/service.rs
Normal file
@@ -0,0 +1,143 @@
|
||||
use crate::calendar::methods::get::route_get;
|
||||
use crate::calendar::methods::mkcalendar::route_mkcalendar;
|
||||
use crate::calendar::methods::report::route_report_calendar;
|
||||
use crate::calendar::resource::CalendarResource;
|
||||
use crate::calendar_object::CalendarObjectResourceService;
|
||||
use crate::calendar_object::resource::CalendarObjectResource;
|
||||
use crate::{CalDavPrincipalUri, Error};
|
||||
use async_trait::async_trait;
|
||||
use axum::Router;
|
||||
use axum::extract::Request;
|
||||
use axum::handler::Handler;
|
||||
use axum::response::Response;
|
||||
use futures_util::future::BoxFuture;
|
||||
use rustical_dav::resource::{AxumMethods, ResourceService};
|
||||
use rustical_store::auth::User;
|
||||
use rustical_store::{CalendarStore, SubscriptionStore};
|
||||
use std::convert::Infallible;
|
||||
use std::sync::Arc;
|
||||
use tower::Service;
|
||||
|
||||
pub struct CalendarResourceService<C: CalendarStore, S: SubscriptionStore> {
|
||||
pub(crate) cal_store: Arc<C>,
|
||||
pub(crate) sub_store: Arc<S>,
|
||||
}
|
||||
|
||||
impl<C: CalendarStore, S: SubscriptionStore> Clone for CalendarResourceService<C, S> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
cal_store: self.cal_store.clone(),
|
||||
sub_store: self.sub_store.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: CalendarStore, S: SubscriptionStore> CalendarResourceService<C, S> {
|
||||
pub fn new(cal_store: Arc<C>, sub_store: Arc<S>) -> Self {
|
||||
Self {
|
||||
cal_store,
|
||||
sub_store,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<C: CalendarStore, S: SubscriptionStore> ResourceService for CalendarResourceService<C, S> {
|
||||
type MemberType = CalendarObjectResource;
|
||||
type PathComponents = (String, String); // principal, calendar_id
|
||||
type Resource = CalendarResource;
|
||||
type Error = Error;
|
||||
type Principal = User;
|
||||
type PrincipalUri = CalDavPrincipalUri;
|
||||
|
||||
const DAV_HEADER: &str = "1, 3, access-control, calendar-access";
|
||||
|
||||
async fn get_resource(
|
||||
&self,
|
||||
(principal, cal_id): &Self::PathComponents,
|
||||
) -> Result<Self::Resource, Error> {
|
||||
let calendar = self.cal_store.get_calendar(principal, cal_id).await?;
|
||||
Ok(CalendarResource {
|
||||
cal: calendar,
|
||||
read_only: self.cal_store.is_read_only(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_members(
|
||||
&self,
|
||||
(principal, cal_id): &Self::PathComponents,
|
||||
) -> Result<Vec<Self::MemberType>, Self::Error> {
|
||||
Ok(self
|
||||
.cal_store
|
||||
.get_objects(principal, cal_id)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|object| CalendarObjectResource {
|
||||
object,
|
||||
principal: principal.to_owned(),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn save_resource(
|
||||
&self,
|
||||
(principal, cal_id): &Self::PathComponents,
|
||||
file: Self::Resource,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.cal_store
|
||||
.update_calendar(principal.to_owned(), cal_id.to_owned(), file.into())
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete_resource(
|
||||
&self,
|
||||
(principal, cal_id): &Self::PathComponents,
|
||||
use_trashbin: bool,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.cal_store
|
||||
.delete_calendar(principal, cal_id, use_trashbin)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn axum_router<State: Send + Sync + Clone + 'static>(self) -> axum::Router<State> {
|
||||
Router::new()
|
||||
.nest(
|
||||
"/{object_id}",
|
||||
CalendarObjectResourceService::new(self.cal_store.clone()).axum_router(),
|
||||
)
|
||||
.route_service("/", self.axum_service())
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: CalendarStore, S: SubscriptionStore> AxumMethods for CalendarResourceService<C, S> {
|
||||
fn report() -> Option<fn(Self, Request) -> BoxFuture<'static, Result<Response, Infallible>>> {
|
||||
Some(|state, req| {
|
||||
let mut service = Handler::with_state(route_report_calendar::<C, S>, state);
|
||||
Box::pin(Service::call(&mut service, req))
|
||||
})
|
||||
}
|
||||
|
||||
fn get() -> Option<fn(Self, Request) -> BoxFuture<'static, Result<Response, Infallible>>> {
|
||||
Some(|state, req| {
|
||||
let mut service = Handler::with_state(route_get::<C, S>, state);
|
||||
Box::pin(Service::call(&mut service, req))
|
||||
})
|
||||
}
|
||||
|
||||
fn mkcalendar() -> Option<fn(Self, Request) -> BoxFuture<'static, Result<Response, Infallible>>>
|
||||
{
|
||||
Some(|state, req| {
|
||||
let mut service = Handler::with_state(route_mkcalendar::<C, S>, state);
|
||||
Box::pin(Service::call(&mut service, req))
|
||||
})
|
||||
}
|
||||
|
||||
fn mkcol() -> Option<fn(Self, Request) -> BoxFuture<'static, Result<Response, Infallible>>> {
|
||||
Some(|state, req| {
|
||||
let mut service = Handler::with_state(route_mkcalendar::<C, S>, state);
|
||||
Box::pin(Service::call(&mut service, req))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,74 +1,85 @@
|
||||
use crate::Error;
|
||||
use actix_web::HttpRequest;
|
||||
use actix_web::HttpResponse;
|
||||
use actix_web::http::header;
|
||||
use actix_web::http::header::HeaderValue;
|
||||
use actix_web::web::{Data, Path};
|
||||
use crate::calendar_object::{CalendarObjectPathComponents, CalendarObjectResourceService};
|
||||
use crate::error::Precondition;
|
||||
use axum::body::Body;
|
||||
use axum::extract::{Path, State};
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use axum_extra::TypedHeader;
|
||||
use headers::{ContentType, ETag, HeaderMapExt, IfNoneMatch};
|
||||
use http::{HeaderMap, StatusCode};
|
||||
use rustical_ical::CalendarObject;
|
||||
use rustical_store::CalendarStore;
|
||||
use rustical_store::auth::User;
|
||||
use rustical_store::{CalendarObject, CalendarStore};
|
||||
use std::str::FromStr;
|
||||
use tracing::instrument;
|
||||
use tracing_actix_web::RootSpan;
|
||||
|
||||
use super::resource::CalendarObjectPathComponents;
|
||||
|
||||
#[instrument(parent = root_span.id(), skip(store, root_span))]
|
||||
#[instrument(skip(cal_store))]
|
||||
pub async fn get_event<C: CalendarStore>(
|
||||
path: Path<CalendarObjectPathComponents>,
|
||||
store: Data<C>,
|
||||
user: User,
|
||||
root_span: RootSpan,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let CalendarObjectPathComponents {
|
||||
Path(CalendarObjectPathComponents {
|
||||
principal,
|
||||
calendar_id,
|
||||
object_id,
|
||||
} = path.into_inner();
|
||||
|
||||
}): Path<CalendarObjectPathComponents>,
|
||||
State(CalendarObjectResourceService { cal_store }): State<CalendarObjectResourceService<C>>,
|
||||
user: User,
|
||||
) -> Result<Response, Error> {
|
||||
if !user.is_principal(&principal) {
|
||||
return Ok(HttpResponse::Unauthorized().body(""));
|
||||
return Err(crate::Error::Unauthorized);
|
||||
}
|
||||
|
||||
let calendar = store.get_calendar(&principal, &calendar_id).await?;
|
||||
let calendar = cal_store.get_calendar(&principal, &calendar_id).await?;
|
||||
if !user.is_principal(&calendar.principal) {
|
||||
return Ok(HttpResponse::Unauthorized().body(""));
|
||||
return Err(crate::Error::Unauthorized);
|
||||
}
|
||||
|
||||
let event = store
|
||||
let event = cal_store
|
||||
.get_object(&principal, &calendar_id, &object_id)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Ok()
|
||||
.insert_header(("ETag", event.get_etag()))
|
||||
.insert_header(("Content-Type", "text/calendar"))
|
||||
.body(event.get_ics().to_owned()))
|
||||
let mut resp = Response::builder().status(StatusCode::OK);
|
||||
let hdrs = resp.headers_mut().unwrap();
|
||||
hdrs.typed_insert(ETag::from_str(&event.get_etag()).unwrap());
|
||||
hdrs.typed_insert(ContentType::from_str("text/calendar").unwrap());
|
||||
Ok(resp.body(Body::new(event.get_ics().to_owned())).unwrap())
|
||||
}
|
||||
|
||||
#[instrument(parent = root_span.id(), skip(store, req, root_span))]
|
||||
#[instrument(skip(cal_store))]
|
||||
pub async fn put_event<C: CalendarStore>(
|
||||
path: Path<CalendarObjectPathComponents>,
|
||||
store: Data<C>,
|
||||
body: String,
|
||||
user: User,
|
||||
req: HttpRequest,
|
||||
root_span: RootSpan,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let CalendarObjectPathComponents {
|
||||
Path(CalendarObjectPathComponents {
|
||||
principal,
|
||||
calendar_id,
|
||||
object_id,
|
||||
} = path.into_inner();
|
||||
|
||||
}): Path<CalendarObjectPathComponents>,
|
||||
State(CalendarObjectResourceService { cal_store }): State<CalendarObjectResourceService<C>>,
|
||||
user: User,
|
||||
mut if_none_match: Option<TypedHeader<IfNoneMatch>>,
|
||||
header_map: HeaderMap,
|
||||
body: String,
|
||||
) -> Result<Response, Error> {
|
||||
if !user.is_principal(&principal) {
|
||||
return Ok(HttpResponse::Unauthorized().body(""));
|
||||
return Err(crate::Error::Unauthorized);
|
||||
}
|
||||
|
||||
let overwrite =
|
||||
Some(&HeaderValue::from_static("*")) != req.headers().get(header::IF_NONE_MATCH);
|
||||
// https://github.com/hyperium/headers/issues/204
|
||||
if !header_map.contains_key("If-None-Match") {
|
||||
if_none_match = None;
|
||||
}
|
||||
|
||||
let object = CalendarObject::from_ics(object_id, body)?;
|
||||
store
|
||||
let overwrite = if let Some(TypedHeader(if_none_match)) = if_none_match {
|
||||
if_none_match == IfNoneMatch::any()
|
||||
} else {
|
||||
true
|
||||
};
|
||||
|
||||
let object = match CalendarObject::from_ics(object_id, body) {
|
||||
Ok(obj) => obj,
|
||||
Err(_) => {
|
||||
return Err(Error::PreconditionFailed(Precondition::ValidCalendarData));
|
||||
}
|
||||
};
|
||||
cal_store
|
||||
.put_object(principal, calendar_id, object, overwrite)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Created().body(""))
|
||||
Ok(StatusCode::CREATED.into_response())
|
||||
}
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
pub mod methods;
|
||||
pub mod resource;
|
||||
mod service;
|
||||
pub use service::*;
|
||||
mod prop;
|
||||
pub use prop::*;
|
||||
|
||||
45
crates/caldav/src/calendar_object/prop.rs
Normal file
45
crates/caldav/src/calendar_object/prop.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
use rustical_dav::extensions::CommonPropertiesProp;
|
||||
use rustical_ical::UtcDateTime;
|
||||
use rustical_xml::{EnumVariants, PropName, XmlDeserialize, XmlSerialize};
|
||||
|
||||
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, PropName)]
|
||||
#[xml(unit_variants_ident = "CalendarObjectPropName")]
|
||||
pub enum CalendarObjectProp {
|
||||
// WebDAV (RFC 2518)
|
||||
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
|
||||
Getetag(String),
|
||||
#[xml(ns = "rustical_dav::namespace::NS_DAV", skip_deserializing)]
|
||||
Getcontenttype(&'static str),
|
||||
|
||||
// CalDAV (RFC 4791)
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
||||
#[xml(prop = "CalendarData")]
|
||||
CalendarData(String),
|
||||
}
|
||||
|
||||
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, PropName)]
|
||||
#[xml(unit_variants_ident = "CalendarObjectPropWrapperName", untagged)]
|
||||
pub enum CalendarObjectPropWrapper {
|
||||
CalendarObject(CalendarObjectProp),
|
||||
Common(CommonPropertiesProp),
|
||||
}
|
||||
|
||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub(crate) struct ExpandElement {
|
||||
#[xml(ty = "attr")]
|
||||
pub(crate) start: UtcDateTime,
|
||||
#[xml(ty = "attr")]
|
||||
pub(crate) end: UtcDateTime,
|
||||
}
|
||||
|
||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq, Default, Eq, Hash)]
|
||||
pub struct CalendarData {
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
||||
pub(crate) comp: Option<()>,
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
||||
pub(crate) expand: Option<ExpandElement>,
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
||||
pub(crate) limit_recurrence_set: Option<()>,
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
||||
pub(crate) limit_freebusy_set: Option<()>,
|
||||
}
|
||||
@@ -1,49 +1,14 @@
|
||||
use super::methods::{get_event, put_event};
|
||||
use crate::{Error, principal::PrincipalResource};
|
||||
use actix_web::dev::ResourceMap;
|
||||
use async_trait::async_trait;
|
||||
use super::prop::*;
|
||||
use crate::Error;
|
||||
use derive_more::derive::{From, Into};
|
||||
use rustical_dav::{
|
||||
extensions::{CommonPropertiesExtension, CommonPropertiesProp},
|
||||
extensions::CommonPropertiesExtension,
|
||||
privileges::UserPrivilegeSet,
|
||||
resource::{Resource, ResourceService},
|
||||
resource::{PrincipalUri, Resource, ResourceName},
|
||||
xml::Resourcetype,
|
||||
};
|
||||
use rustical_store::{CalendarObject, CalendarStore, auth::User};
|
||||
use rustical_xml::{EnumUnitVariants, EnumVariants, XmlDeserialize, XmlSerialize};
|
||||
use serde::Deserialize;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct CalendarObjectResourceService<C: CalendarStore> {
|
||||
cal_store: Arc<C>,
|
||||
}
|
||||
|
||||
impl<C: CalendarStore> CalendarObjectResourceService<C> {
|
||||
pub fn new(cal_store: Arc<C>) -> Self {
|
||||
Self { cal_store }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, EnumUnitVariants)]
|
||||
#[xml(unit_variants_ident = "CalendarObjectPropName")]
|
||||
pub enum CalendarObjectProp {
|
||||
// WebDAV (RFC 2518)
|
||||
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
|
||||
Getetag(String),
|
||||
#[xml(ns = "rustical_dav::namespace::NS_DAV", skip_deserializing)]
|
||||
Getcontenttype(&'static str),
|
||||
|
||||
// CalDAV (RFC 4791)
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
||||
CalendarData(String),
|
||||
}
|
||||
|
||||
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, EnumUnitVariants)]
|
||||
#[xml(unit_variants_ident = "CalendarObjectPropWrapperName", untagged)]
|
||||
pub enum CalendarObjectPropWrapper {
|
||||
CalendarObject(CalendarObjectProp),
|
||||
Common(CommonPropertiesProp),
|
||||
}
|
||||
use rustical_ical::CalendarObject;
|
||||
use rustical_store::auth::User;
|
||||
|
||||
#[derive(Clone, From, Into)]
|
||||
pub struct CalendarObjectResource {
|
||||
@@ -51,8 +16,10 @@ pub struct CalendarObjectResource {
|
||||
pub principal: String,
|
||||
}
|
||||
|
||||
impl CommonPropertiesExtension for CalendarObjectResource {
|
||||
type PrincipalResource = PrincipalResource;
|
||||
impl ResourceName for CalendarObjectResource {
|
||||
fn get_name(&self) -> String {
|
||||
format!("{}.ics", self.object.get_id())
|
||||
}
|
||||
}
|
||||
|
||||
impl Resource for CalendarObjectResource {
|
||||
@@ -60,13 +27,15 @@ impl Resource for CalendarObjectResource {
|
||||
type Error = Error;
|
||||
type Principal = User;
|
||||
|
||||
const IS_COLLECTION: bool = false;
|
||||
|
||||
fn get_resourcetype(&self) -> Resourcetype {
|
||||
Resourcetype(&[])
|
||||
}
|
||||
|
||||
fn get_prop(
|
||||
&self,
|
||||
rmap: &ResourceMap,
|
||||
puri: &impl PrincipalUri,
|
||||
user: &User,
|
||||
prop: &CalendarObjectPropWrapperName,
|
||||
) -> Result<Self::Prop, Self::Error> {
|
||||
@@ -76,8 +45,15 @@ impl Resource for CalendarObjectResource {
|
||||
CalendarObjectPropName::Getetag => {
|
||||
CalendarObjectProp::Getetag(self.object.get_etag())
|
||||
}
|
||||
CalendarObjectPropName::CalendarData => {
|
||||
CalendarObjectProp::CalendarData(self.object.get_ics().to_owned())
|
||||
CalendarObjectPropName::CalendarData(CalendarData { expand, .. }) => {
|
||||
CalendarObjectProp::CalendarData(if let Some(expand) = expand.as_ref() {
|
||||
self.object.expand_recurrence(
|
||||
Some(expand.start.to_utc()),
|
||||
Some(expand.end.to_utc()),
|
||||
)?
|
||||
} else {
|
||||
self.object.get_ics().to_owned()
|
||||
})
|
||||
}
|
||||
CalendarObjectPropName::Getcontenttype => {
|
||||
CalendarObjectProp::Getcontenttype("text/calendar;charset=utf-8")
|
||||
@@ -85,7 +61,7 @@ impl Resource for CalendarObjectResource {
|
||||
})
|
||||
}
|
||||
CalendarObjectPropWrapperName::Common(prop) => CalendarObjectPropWrapper::Common(
|
||||
CommonPropertiesExtension::get_prop(self, rmap, user, prop)?,
|
||||
CommonPropertiesExtension::get_prop(self, puri, user, prop)?,
|
||||
),
|
||||
})
|
||||
}
|
||||
@@ -104,57 +80,3 @@ impl Resource for CalendarObjectResource {
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct CalendarObjectPathComponents {
|
||||
pub principal: String,
|
||||
pub calendar_id: String,
|
||||
pub object_id: String,
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl<C: CalendarStore> ResourceService for CalendarObjectResourceService<C> {
|
||||
type PathComponents = CalendarObjectPathComponents;
|
||||
type Resource = CalendarObjectResource;
|
||||
type MemberType = CalendarObjectResource;
|
||||
type Error = Error;
|
||||
type Principal = User;
|
||||
|
||||
async fn get_resource(
|
||||
&self,
|
||||
CalendarObjectPathComponents {
|
||||
principal,
|
||||
calendar_id,
|
||||
object_id,
|
||||
}: &Self::PathComponents,
|
||||
) -> Result<Self::Resource, Self::Error> {
|
||||
let object = self
|
||||
.cal_store
|
||||
.get_object(principal, calendar_id, object_id)
|
||||
.await?;
|
||||
Ok(CalendarObjectResource {
|
||||
object,
|
||||
principal: principal.to_owned(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn delete_resource(
|
||||
&self,
|
||||
CalendarObjectPathComponents {
|
||||
principal,
|
||||
calendar_id,
|
||||
object_id,
|
||||
}: &Self::PathComponents,
|
||||
use_trashbin: bool,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.cal_store
|
||||
.delete_object(principal, calendar_id, object_id, use_trashbin)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn actix_additional_routes(res: actix_web::Resource) -> actix_web::Resource {
|
||||
res.get(get_event::<C>).put(put_event::<C>)
|
||||
}
|
||||
}
|
||||
|
||||
113
crates/caldav/src/calendar_object/service.rs
Normal file
113
crates/caldav/src/calendar_object/service.rs
Normal file
@@ -0,0 +1,113 @@
|
||||
use crate::{
|
||||
CalDavPrincipalUri, Error,
|
||||
calendar_object::{
|
||||
methods::{get_event, put_event},
|
||||
resource::CalendarObjectResource,
|
||||
},
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use axum::{extract::Request, handler::Handler, response::Response};
|
||||
use futures_util::future::BoxFuture;
|
||||
use rustical_dav::resource::{AxumMethods, ResourceService};
|
||||
use rustical_store::{CalendarStore, auth::User};
|
||||
use serde::{Deserialize, Deserializer};
|
||||
use std::{convert::Infallible, sync::Arc};
|
||||
use tower::Service;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct CalendarObjectPathComponents {
|
||||
pub principal: String,
|
||||
pub calendar_id: String,
|
||||
#[serde(deserialize_with = "deserialize_ics_name")]
|
||||
pub object_id: String,
|
||||
}
|
||||
|
||||
pub struct CalendarObjectResourceService<C: CalendarStore> {
|
||||
pub(crate) cal_store: Arc<C>,
|
||||
}
|
||||
|
||||
impl<C: CalendarStore> Clone for CalendarObjectResourceService<C> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
cal_store: self.cal_store.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: CalendarStore> CalendarObjectResourceService<C> {
|
||||
pub fn new(cal_store: Arc<C>) -> Self {
|
||||
Self { cal_store }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<C: CalendarStore> ResourceService for CalendarObjectResourceService<C> {
|
||||
type PathComponents = CalendarObjectPathComponents;
|
||||
type Resource = CalendarObjectResource;
|
||||
type MemberType = CalendarObjectResource;
|
||||
type Error = Error;
|
||||
type Principal = User;
|
||||
type PrincipalUri = CalDavPrincipalUri;
|
||||
|
||||
const DAV_HEADER: &str = "1, 3, access-control, calendar-access";
|
||||
|
||||
async fn get_resource(
|
||||
&self,
|
||||
CalendarObjectPathComponents {
|
||||
principal,
|
||||
calendar_id,
|
||||
object_id,
|
||||
}: &Self::PathComponents,
|
||||
) -> Result<Self::Resource, Self::Error> {
|
||||
let object = self
|
||||
.cal_store
|
||||
.get_object(principal, calendar_id, object_id)
|
||||
.await?;
|
||||
Ok(CalendarObjectResource {
|
||||
object,
|
||||
principal: principal.to_owned(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn delete_resource(
|
||||
&self,
|
||||
CalendarObjectPathComponents {
|
||||
principal,
|
||||
calendar_id,
|
||||
object_id,
|
||||
}: &Self::PathComponents,
|
||||
use_trashbin: bool,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.cal_store
|
||||
.delete_object(principal, calendar_id, object_id, use_trashbin)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: CalendarStore> AxumMethods for CalendarObjectResourceService<C> {
|
||||
fn get() -> Option<fn(Self, Request) -> BoxFuture<'static, Result<Response, Infallible>>> {
|
||||
Some(|state, req| {
|
||||
let mut service = Handler::with_state(get_event::<C>, state);
|
||||
Box::pin(Service::call(&mut service, req))
|
||||
})
|
||||
}
|
||||
fn put() -> Option<fn(Self, Request) -> BoxFuture<'static, Result<Response, Infallible>>> {
|
||||
Some(|state, req| {
|
||||
let mut service = Handler::with_state(put_event::<C>, state);
|
||||
Box::pin(Service::call(&mut service, req))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_ics_name<'de, D>(deserializer: D) -> Result<String, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let name: String = Deserialize::deserialize(deserializer)?;
|
||||
if let Some(object_id) = name.strip_suffix(".ics") {
|
||||
Ok(object_id.to_owned())
|
||||
} else {
|
||||
Err(serde::de::Error::custom("Missing .ics extension"))
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,26 @@
|
||||
use crate::Error;
|
||||
use crate::calendar::resource::CalendarResource;
|
||||
use crate::principal::PrincipalResource;
|
||||
use actix_web::dev::ResourceMap;
|
||||
use async_trait::async_trait;
|
||||
use rustical_dav::extensions::{CommonPropertiesExtension, CommonPropertiesProp};
|
||||
use rustical_dav::extensions::CommonPropertiesExtension;
|
||||
use rustical_dav::privileges::UserPrivilegeSet;
|
||||
use rustical_dav::resource::{Resource, ResourceService};
|
||||
use rustical_dav::resource::{PrincipalUri, Resource, ResourceName};
|
||||
use rustical_dav::xml::{Resourcetype, ResourcetypeInner};
|
||||
use rustical_store::CalendarStore;
|
||||
use rustical_store::auth::User;
|
||||
use rustical_xml::{EnumUnitVariants, EnumVariants, XmlDeserialize, XmlSerialize};
|
||||
use std::sync::Arc;
|
||||
|
||||
mod service;
|
||||
pub use service::*;
|
||||
mod prop;
|
||||
pub use prop::*;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CalendarSetResource {
|
||||
pub(crate) principal: String,
|
||||
pub(crate) read_only: bool,
|
||||
pub(crate) name: &'static str,
|
||||
}
|
||||
|
||||
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, EnumUnitVariants)]
|
||||
#[xml(unit_variants_ident = "PrincipalPropWrapperName", untagged)]
|
||||
pub enum PrincipalPropWrapper {
|
||||
Common(CommonPropertiesProp),
|
||||
}
|
||||
|
||||
impl CommonPropertiesExtension for CalendarSetResource {
|
||||
type PrincipalResource = PrincipalResource;
|
||||
impl ResourceName for CalendarSetResource {
|
||||
fn get_name(&self) -> String {
|
||||
self.name.to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
impl Resource for CalendarSetResource {
|
||||
@@ -33,6 +28,8 @@ impl Resource for CalendarSetResource {
|
||||
type Error = Error;
|
||||
type Principal = User;
|
||||
|
||||
const IS_COLLECTION: bool = true;
|
||||
|
||||
fn get_resourcetype(&self) -> Resourcetype {
|
||||
Resourcetype(&[ResourcetypeInner(
|
||||
Some(rustical_dav::namespace::NS_DAV),
|
||||
@@ -42,13 +39,13 @@ impl Resource for CalendarSetResource {
|
||||
|
||||
fn get_prop(
|
||||
&self,
|
||||
rmap: &ResourceMap,
|
||||
puri: &impl PrincipalUri,
|
||||
user: &User,
|
||||
prop: &PrincipalPropWrapperName,
|
||||
) -> Result<Self::Prop, Self::Error> {
|
||||
Ok(match prop {
|
||||
PrincipalPropWrapperName::Common(prop) => PrincipalPropWrapper::Common(
|
||||
<Self as CommonPropertiesExtension>::get_prop(self, rmap, user, prop)?,
|
||||
<Self as CommonPropertiesExtension>::get_prop(self, puri, user, prop)?,
|
||||
),
|
||||
})
|
||||
}
|
||||
@@ -65,51 +62,3 @@ impl Resource for CalendarSetResource {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CalendarSetResourceService<C: CalendarStore> {
|
||||
cal_store: Arc<C>,
|
||||
}
|
||||
|
||||
impl<C: CalendarStore> CalendarSetResourceService<C> {
|
||||
pub fn new(cal_store: Arc<C>) -> Self {
|
||||
Self { cal_store }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl<C: CalendarStore> ResourceService for CalendarSetResourceService<C> {
|
||||
type PathComponents = (String,);
|
||||
type MemberType = CalendarResource;
|
||||
type Resource = CalendarSetResource;
|
||||
type Error = Error;
|
||||
type Principal = User;
|
||||
|
||||
async fn get_resource(
|
||||
&self,
|
||||
(principal,): &Self::PathComponents,
|
||||
) -> Result<Self::Resource, Self::Error> {
|
||||
Ok(CalendarSetResource {
|
||||
principal: principal.to_owned(),
|
||||
read_only: self.cal_store.is_read_only(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_members(
|
||||
&self,
|
||||
(principal,): &Self::PathComponents,
|
||||
) -> Result<Vec<(String, Self::MemberType)>, Self::Error> {
|
||||
let calendars = self.cal_store.get_calendars(principal).await?;
|
||||
Ok(calendars
|
||||
.into_iter()
|
||||
.map(|cal| {
|
||||
(
|
||||
cal.id.to_owned(),
|
||||
CalendarResource {
|
||||
cal,
|
||||
read_only: self.cal_store.is_read_only(),
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
8
crates/caldav/src/calendar_set/prop.rs
Normal file
8
crates/caldav/src/calendar_set/prop.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
use rustical_dav::extensions::CommonPropertiesProp;
|
||||
use rustical_xml::{EnumVariants, PropName, XmlDeserialize, XmlSerialize};
|
||||
|
||||
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, PropName)]
|
||||
#[xml(unit_variants_ident = "PrincipalPropWrapperName", untagged)]
|
||||
pub enum PrincipalPropWrapper {
|
||||
Common(CommonPropertiesProp),
|
||||
}
|
||||
84
crates/caldav/src/calendar_set/service.rs
Normal file
84
crates/caldav/src/calendar_set/service.rs
Normal file
@@ -0,0 +1,84 @@
|
||||
use crate::calendar::CalendarResourceService;
|
||||
use crate::calendar::resource::CalendarResource;
|
||||
use crate::calendar_set::CalendarSetResource;
|
||||
use crate::{CalDavPrincipalUri, Error};
|
||||
use async_trait::async_trait;
|
||||
use axum::Router;
|
||||
use rustical_dav::resource::{AxumMethods, ResourceService};
|
||||
use rustical_store::auth::User;
|
||||
use rustical_store::{CalendarStore, SubscriptionStore};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct CalendarSetResourceService<C: CalendarStore, S: SubscriptionStore> {
|
||||
name: &'static str,
|
||||
cal_store: Arc<C>,
|
||||
sub_store: Arc<S>,
|
||||
}
|
||||
|
||||
impl<C: CalendarStore, S: SubscriptionStore> Clone for CalendarSetResourceService<C, S> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
name: self.name,
|
||||
cal_store: self.cal_store.clone(),
|
||||
sub_store: self.sub_store.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: CalendarStore, S: SubscriptionStore> CalendarSetResourceService<C, S> {
|
||||
pub fn new(name: &'static str, cal_store: Arc<C>, sub_store: Arc<S>) -> Self {
|
||||
Self {
|
||||
name,
|
||||
cal_store,
|
||||
sub_store,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<C: CalendarStore, S: SubscriptionStore> ResourceService for CalendarSetResourceService<C, S> {
|
||||
type PathComponents = (String,);
|
||||
type MemberType = CalendarResource;
|
||||
type Resource = CalendarSetResource;
|
||||
type Error = Error;
|
||||
type Principal = User;
|
||||
type PrincipalUri = CalDavPrincipalUri;
|
||||
|
||||
const DAV_HEADER: &str = "1, 3, access-control, extended-mkcol, calendar-access";
|
||||
|
||||
async fn get_resource(
|
||||
&self,
|
||||
(principal,): &Self::PathComponents,
|
||||
) -> Result<Self::Resource, Self::Error> {
|
||||
Ok(CalendarSetResource {
|
||||
principal: principal.to_owned(),
|
||||
read_only: self.cal_store.is_read_only(),
|
||||
name: self.name,
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_members(
|
||||
&self,
|
||||
(principal,): &Self::PathComponents,
|
||||
) -> Result<Vec<Self::MemberType>, Self::Error> {
|
||||
let calendars = self.cal_store.get_calendars(principal).await?;
|
||||
Ok(calendars
|
||||
.into_iter()
|
||||
.map(|cal| CalendarResource {
|
||||
cal,
|
||||
read_only: self.cal_store.is_read_only(),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn axum_router<State: Send + Sync + Clone + 'static>(self) -> axum::Router<State> {
|
||||
Router::new()
|
||||
.nest(
|
||||
"/{calendar_id}",
|
||||
CalendarResourceService::new(self.cal_store.clone(), self.sub_store.clone())
|
||||
.axum_router(),
|
||||
)
|
||||
.route_service("/", self.axum_service())
|
||||
}
|
||||
}
|
||||
impl<C: CalendarStore, S: SubscriptionStore> AxumMethods for CalendarSetResourceService<C, S> {}
|
||||
@@ -1,6 +1,34 @@
|
||||
use actix_web::{HttpResponse, http::StatusCode};
|
||||
use axum::{
|
||||
body::Body,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use headers::{ContentType, HeaderMapExt};
|
||||
use http::StatusCode;
|
||||
use rustical_xml::{XmlSerialize, XmlSerializeRoot};
|
||||
use tracing::error;
|
||||
|
||||
#[derive(Debug, thiserror::Error, XmlSerialize)]
|
||||
pub enum Precondition {
|
||||
#[error("valid-calendar-data")]
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
||||
ValidCalendarData,
|
||||
}
|
||||
|
||||
impl IntoResponse for Precondition {
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
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 error = rustical_dav::xml::ErrorElement(&self);
|
||||
if let Err(err) = error.serialize_root(&mut writer) {
|
||||
return rustical_dav::Error::from(err).into_response();
|
||||
}
|
||||
let mut res = Response::builder().status(StatusCode::PRECONDITION_FAILED);
|
||||
res.headers_mut().unwrap().typed_insert(ContentType::xml());
|
||||
res.body(Body::from(output)).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("Unauthorized")]
|
||||
@@ -23,32 +51,38 @@ pub enum Error {
|
||||
|
||||
#[error(transparent)]
|
||||
XmlDecodeError(#[from] rustical_xml::XmlError),
|
||||
|
||||
#[error(transparent)]
|
||||
IcalError(#[from] rustical_ical::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
PreconditionFailed(Precondition),
|
||||
}
|
||||
|
||||
impl actix_web::ResponseError for Error {
|
||||
fn status_code(&self) -> actix_web::http::StatusCode {
|
||||
impl Error {
|
||||
pub fn status_code(&self) -> StatusCode {
|
||||
match self {
|
||||
Error::StoreError(err) => match err {
|
||||
rustical_store::Error::NotFound => StatusCode::NOT_FOUND,
|
||||
rustical_store::Error::InvalidData(_) => StatusCode::BAD_REQUEST,
|
||||
rustical_store::Error::AlreadyExists => StatusCode::CONFLICT,
|
||||
rustical_store::Error::ParserError(_) => StatusCode::BAD_REQUEST,
|
||||
rustical_store::Error::ReadOnly => StatusCode::FORBIDDEN,
|
||||
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
},
|
||||
Error::ChronoParseError(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Error::DavError(err) => err.status_code(),
|
||||
Error::DavError(err) => StatusCode::try_from(err.status_code().as_u16())
|
||||
.expect("Just converting between versions"),
|
||||
Error::Unauthorized => StatusCode::UNAUTHORIZED,
|
||||
Error::XmlDecodeError(_) => StatusCode::BAD_REQUEST,
|
||||
Error::NotImplemented => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Error::NotFound => StatusCode::NOT_FOUND,
|
||||
}
|
||||
}
|
||||
fn error_response(&self) -> actix_web::HttpResponse<actix_web::body::BoxBody> {
|
||||
error!("Error: {self}");
|
||||
match self {
|
||||
Error::DavError(err) => err.error_response(),
|
||||
_ => HttpResponse::build(self.status_code()).body(self.to_string()),
|
||||
Error::IcalError(err) => err.status_code(),
|
||||
Error::PreconditionFailed(_err) => StatusCode::PRECONDITION_FAILED,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponse for Error {
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
(self.status_code(), self.to_string()).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,99 +1,63 @@
|
||||
use actix_web::HttpResponse;
|
||||
use actix_web::dev::{HttpServiceFactory, ServiceResponse};
|
||||
use actix_web::http::header::{self, HeaderName, HeaderValue};
|
||||
use actix_web::http::{Method, StatusCode};
|
||||
use actix_web::middleware::{ErrorHandlerResponse, ErrorHandlers};
|
||||
use actix_web::web::{self, Data};
|
||||
use calendar::resource::CalendarResourceService;
|
||||
use calendar_object::resource::CalendarObjectResourceService;
|
||||
use calendar_set::CalendarSetResourceService;
|
||||
use principal::{PrincipalResource, PrincipalResourceService};
|
||||
use rustical_dav::resource::{NamedRoute, ResourceService, ResourceServiceRoute};
|
||||
use axum::response::Redirect;
|
||||
use axum::routing::any;
|
||||
use axum::{Extension, Router};
|
||||
use derive_more::Constructor;
|
||||
use principal::PrincipalResourceService;
|
||||
use rustical_dav::resource::{PrincipalUri, ResourceService};
|
||||
use rustical_dav::resources::RootResourceService;
|
||||
use rustical_store::auth::{AuthenticationMiddleware, AuthenticationProvider, User};
|
||||
use rustical_store::auth::middleware::AuthenticationLayer;
|
||||
use rustical_store::auth::{AuthenticationProvider, User};
|
||||
use rustical_store::{AddressbookStore, CalendarStore, ContactBirthdayStore, SubscriptionStore};
|
||||
use std::sync::Arc;
|
||||
use subscription::subscription_resource;
|
||||
|
||||
pub mod calendar;
|
||||
pub mod calendar_object;
|
||||
pub mod calendar_set;
|
||||
pub mod error;
|
||||
pub mod principal;
|
||||
mod subscription;
|
||||
// mod subscription;
|
||||
|
||||
pub use error::Error;
|
||||
|
||||
pub fn caldav_service<
|
||||
#[derive(Debug, Clone, Constructor)]
|
||||
pub struct CalDavPrincipalUri(&'static str);
|
||||
|
||||
impl PrincipalUri for CalDavPrincipalUri {
|
||||
fn principal_uri(&self, principal: &str) -> String {
|
||||
format!("{}/principal/{}/", self.0, principal)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn caldav_router<
|
||||
AP: AuthenticationProvider,
|
||||
AS: AddressbookStore,
|
||||
C: CalendarStore,
|
||||
S: SubscriptionStore,
|
||||
>(
|
||||
prefix: &'static str,
|
||||
auth_provider: Arc<AP>,
|
||||
store: Arc<C>,
|
||||
addr_store: Arc<AS>,
|
||||
subscription_store: Arc<S>,
|
||||
) -> impl HttpServiceFactory {
|
||||
) -> Router {
|
||||
let birthday_store = Arc::new(ContactBirthdayStore::new(addr_store));
|
||||
let principal_service = PrincipalResourceService {
|
||||
auth_provider: auth_provider.clone(),
|
||||
sub_store: subscription_store.clone(),
|
||||
birthday_store: birthday_store.clone(),
|
||||
cal_store: store.clone(),
|
||||
};
|
||||
|
||||
web::scope("")
|
||||
.wrap(AuthenticationMiddleware::new(auth_provider.clone()))
|
||||
.wrap(
|
||||
ErrorHandlers::new().handler(StatusCode::METHOD_NOT_ALLOWED, |res| {
|
||||
Ok(ErrorHandlerResponse::Response(
|
||||
if res.request().method() == Method::OPTIONS {
|
||||
let mut response = HttpResponse::Ok();
|
||||
response.insert_header((
|
||||
HeaderName::from_static("dav"),
|
||||
// https://datatracker.ietf.org/doc/html/rfc4918#section-18
|
||||
HeaderValue::from_static(
|
||||
"1, 3, access-control, calendar-access, extended-mkcol, calendar-no-timezone, webdav-push",
|
||||
),
|
||||
));
|
||||
|
||||
if let Some(allow) = res.headers().get(header::ALLOW) {
|
||||
response.insert_header((header::ALLOW, allow.to_owned()));
|
||||
}
|
||||
ServiceResponse::new(res.into_parts().0, response.finish()).map_into_right_body()
|
||||
} else {
|
||||
res.map_into_left_body()
|
||||
},
|
||||
))
|
||||
}),
|
||||
)
|
||||
.app_data(Data::from(store.clone()))
|
||||
.app_data(Data::from(birthday_store.clone()))
|
||||
.app_data(Data::from(subscription_store))
|
||||
.service(RootResourceService::<PrincipalResource, User>::default().actix_resource())
|
||||
.service(
|
||||
web::scope("/principal").service(
|
||||
web::scope("/{principal}")
|
||||
.service(PrincipalResourceService{auth_provider, home_set: &[
|
||||
("calendar", false), ("birthdays", true)
|
||||
]}.actix_resource().name(PrincipalResource::route_name()))
|
||||
.service(web::scope("/calendar")
|
||||
.service(CalendarSetResourceService::new(store.clone()).actix_resource())
|
||||
.service(
|
||||
web::scope("/{calendar_id}")
|
||||
.service(
|
||||
ResourceServiceRoute(CalendarResourceService::<_, S>::new(store.clone()))
|
||||
)
|
||||
.service(web::scope("/{object_id}.ics").service(CalendarObjectResourceService::new(store.clone()).actix_resource()
|
||||
))
|
||||
)
|
||||
)
|
||||
.service(web::scope("/birthdays")
|
||||
.service(CalendarSetResourceService::new(birthday_store.clone()).actix_resource())
|
||||
.service(
|
||||
web::scope("/{calendar_id}")
|
||||
.service(
|
||||
ResourceServiceRoute(CalendarResourceService::<_, S>::new(birthday_store.clone()))
|
||||
)
|
||||
.service(web::scope("/{object_id}.ics").service(CalendarObjectResourceService::new(birthday_store.clone()).actix_resource()
|
||||
))
|
||||
)
|
||||
)
|
||||
),
|
||||
).service(subscription_resource::<S>())
|
||||
Router::new()
|
||||
.nest(
|
||||
prefix,
|
||||
RootResourceService::<_, User, CalDavPrincipalUri>::new(principal_service.clone())
|
||||
.axum_router()
|
||||
.layer(AuthenticationLayer::new(auth_provider))
|
||||
.layer(Extension(CalDavPrincipalUri(prefix))),
|
||||
)
|
||||
.route(
|
||||
"/.well-known/caldav",
|
||||
any(async || Redirect::permanent(prefix)),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,75 +1,34 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::Error;
|
||||
use crate::calendar_set::CalendarSetResource;
|
||||
use actix_web::dev::ResourceMap;
|
||||
use async_trait::async_trait;
|
||||
use rustical_dav::extensions::{CommonPropertiesExtension, CommonPropertiesProp};
|
||||
use rustical_dav::extensions::CommonPropertiesExtension;
|
||||
use rustical_dav::privileges::UserPrivilegeSet;
|
||||
use rustical_dav::resource::{NamedRoute, Resource, ResourceService};
|
||||
use rustical_dav::resource::{PrincipalUri, Resource, ResourceName};
|
||||
use rustical_dav::xml::{HrefElement, Resourcetype, ResourcetypeInner};
|
||||
use rustical_store::auth::user::PrincipalType;
|
||||
use rustical_store::auth::{AuthenticationProvider, User};
|
||||
use rustical_xml::{EnumUnitVariants, EnumVariants, XmlDeserialize, XmlSerialize};
|
||||
use rustical_store::auth::User;
|
||||
|
||||
mod service;
|
||||
pub use service::*;
|
||||
mod prop;
|
||||
pub use prop::*;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PrincipalResource {
|
||||
principal: User,
|
||||
home_set: &'static [(&'static str, bool)],
|
||||
home_set: &'static [&'static str],
|
||||
}
|
||||
|
||||
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone)]
|
||||
pub struct CalendarHomeSet(#[xml(ty = "untagged", flatten)] Vec<HrefElement>);
|
||||
|
||||
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, EnumUnitVariants)]
|
||||
#[xml(unit_variants_ident = "PrincipalPropName")]
|
||||
pub enum PrincipalProp {
|
||||
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
|
||||
Displayname(String),
|
||||
|
||||
// Scheduling Extensions to CalDAV (RFC 6638)
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV", skip_deserializing)]
|
||||
CalendarUserType(PrincipalType),
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
||||
CalendarUserAddressSet(HrefElement),
|
||||
|
||||
// WebDAV Access Control (RFC 3744)
|
||||
#[xml(ns = "rustical_dav::namespace::NS_DAV", rename = b"principal-URL")]
|
||||
PrincipalUrl(HrefElement),
|
||||
|
||||
// CalDAV (RFC 4791)
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
||||
CalendarHomeSet(CalendarHomeSet),
|
||||
}
|
||||
|
||||
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, EnumUnitVariants)]
|
||||
#[xml(unit_variants_ident = "PrincipalPropWrapperName", untagged)]
|
||||
pub enum PrincipalPropWrapper {
|
||||
Principal(PrincipalProp),
|
||||
Common(CommonPropertiesProp),
|
||||
}
|
||||
|
||||
impl PrincipalResource {
|
||||
pub fn get_principal_url(rmap: &ResourceMap, principal: &str) -> String {
|
||||
Self::get_url(rmap, vec![principal]).unwrap()
|
||||
impl ResourceName for PrincipalResource {
|
||||
fn get_name(&self) -> String {
|
||||
self.principal.id.to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
impl NamedRoute for PrincipalResource {
|
||||
fn route_name() -> &'static str {
|
||||
"caldav_principal"
|
||||
}
|
||||
}
|
||||
|
||||
impl CommonPropertiesExtension for PrincipalResource {
|
||||
type PrincipalResource = Self;
|
||||
}
|
||||
|
||||
impl Resource for PrincipalResource {
|
||||
type Prop = PrincipalPropWrapper;
|
||||
type Error = Error;
|
||||
type Principal = User;
|
||||
|
||||
const IS_COLLECTION: bool = true;
|
||||
|
||||
fn get_resourcetype(&self) -> Resourcetype {
|
||||
Resourcetype(&[
|
||||
ResourcetypeInner(Some(rustical_dav::namespace::NS_DAV), "collection"),
|
||||
@@ -79,19 +38,19 @@ impl Resource for PrincipalResource {
|
||||
|
||||
fn get_prop(
|
||||
&self,
|
||||
rmap: &ResourceMap,
|
||||
puri: &impl PrincipalUri,
|
||||
user: &User,
|
||||
prop: &PrincipalPropWrapperName,
|
||||
) -> Result<Self::Prop, Self::Error> {
|
||||
let principal_url = Self::get_url(rmap, vec![&self.principal.id]).unwrap();
|
||||
let principal_url = puri.principal_uri(&self.principal.id);
|
||||
|
||||
let home_set = CalendarHomeSet(
|
||||
user.memberships()
|
||||
.into_iter()
|
||||
.map(|principal| Self::get_url(rmap, vec![principal]).unwrap())
|
||||
.map(|principal| puri.principal_uri(principal))
|
||||
.flat_map(|principal_url| {
|
||||
self.home_set.iter().map(move |&(home_name, _read_only)| {
|
||||
HrefElement::new(format!("{}/{}", &principal_url, home_name))
|
||||
self.home_set.iter().map(move |&home_name| {
|
||||
HrefElement::new(format!("{}{}/", &principal_url, home_name))
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
@@ -119,7 +78,7 @@ impl Resource for PrincipalResource {
|
||||
})
|
||||
}
|
||||
PrincipalPropWrapperName::Common(prop) => PrincipalPropWrapper::Common(
|
||||
<Self as CommonPropertiesExtension>::get_prop(self, rmap, user, prop)?,
|
||||
<Self as CommonPropertiesExtension>::get_prop(self, puri, user, prop)?,
|
||||
),
|
||||
})
|
||||
}
|
||||
@@ -134,51 +93,3 @@ impl Resource for PrincipalResource {
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PrincipalResourceService<AP: AuthenticationProvider> {
|
||||
pub auth_provider: Arc<AP>,
|
||||
pub home_set: &'static [(&'static str, bool)],
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl<AP: AuthenticationProvider> ResourceService for PrincipalResourceService<AP> {
|
||||
type PathComponents = (String,);
|
||||
type MemberType = CalendarSetResource;
|
||||
type Resource = PrincipalResource;
|
||||
type Error = Error;
|
||||
type Principal = User;
|
||||
|
||||
async fn get_resource(
|
||||
&self,
|
||||
(principal,): &Self::PathComponents,
|
||||
) -> Result<Self::Resource, Self::Error> {
|
||||
let user = self
|
||||
.auth_provider
|
||||
.get_principal(principal)
|
||||
.await?
|
||||
.ok_or(crate::Error::NotFound)?;
|
||||
Ok(PrincipalResource {
|
||||
principal: user,
|
||||
home_set: self.home_set,
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_members(
|
||||
&self,
|
||||
(principal,): &Self::PathComponents,
|
||||
) -> Result<Vec<(String, Self::MemberType)>, Self::Error> {
|
||||
Ok(self
|
||||
.home_set
|
||||
.iter()
|
||||
.map(|&(set_name, read_only)| {
|
||||
(
|
||||
set_name.to_string(),
|
||||
CalendarSetResource {
|
||||
principal: principal.to_owned(),
|
||||
read_only,
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
34
crates/caldav/src/principal/prop.rs
Normal file
34
crates/caldav/src/principal/prop.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
use rustical_dav::{extensions::CommonPropertiesProp, xml::HrefElement};
|
||||
use rustical_store::auth::user::PrincipalType;
|
||||
use rustical_xml::{EnumVariants, PropName, XmlDeserialize, XmlSerialize};
|
||||
|
||||
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, PropName)]
|
||||
#[xml(unit_variants_ident = "PrincipalPropName")]
|
||||
pub enum PrincipalProp {
|
||||
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
|
||||
Displayname(String),
|
||||
|
||||
// Scheduling Extensions to CalDAV (RFC 6638)
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV", skip_deserializing)]
|
||||
CalendarUserType(PrincipalType),
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
||||
CalendarUserAddressSet(HrefElement),
|
||||
|
||||
// WebDAV Access Control (RFC 3744)
|
||||
#[xml(ns = "rustical_dav::namespace::NS_DAV", rename = b"principal-URL")]
|
||||
PrincipalUrl(HrefElement),
|
||||
|
||||
// CalDAV (RFC 4791)
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
||||
CalendarHomeSet(CalendarHomeSet),
|
||||
}
|
||||
|
||||
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, PropName)]
|
||||
#[xml(unit_variants_ident = "PrincipalPropWrapperName", untagged)]
|
||||
pub enum PrincipalPropWrapper {
|
||||
Principal(PrincipalProp),
|
||||
Common(CommonPropertiesProp),
|
||||
}
|
||||
|
||||
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone)]
|
||||
pub struct CalendarHomeSet(#[xml(ty = "untagged", flatten)] pub(super) Vec<HrefElement>);
|
||||
110
crates/caldav/src/principal/service.rs
Normal file
110
crates/caldav/src/principal/service.rs
Normal file
@@ -0,0 +1,110 @@
|
||||
use crate::calendar_set::{CalendarSetResource, CalendarSetResourceService};
|
||||
use crate::principal::PrincipalResource;
|
||||
use crate::{CalDavPrincipalUri, Error};
|
||||
use async_trait::async_trait;
|
||||
use axum::Router;
|
||||
use rustical_dav::resource::{AxumMethods, ResourceService};
|
||||
use rustical_store::auth::{AuthenticationProvider, User};
|
||||
use rustical_store::{CalendarStore, SubscriptionStore};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PrincipalResourceService<
|
||||
AP: AuthenticationProvider,
|
||||
S: SubscriptionStore,
|
||||
CS: CalendarStore,
|
||||
BS: CalendarStore,
|
||||
> {
|
||||
pub(crate) auth_provider: Arc<AP>,
|
||||
pub(crate) sub_store: Arc<S>,
|
||||
pub(crate) cal_store: Arc<CS>,
|
||||
pub(crate) birthday_store: Arc<BS>,
|
||||
}
|
||||
|
||||
impl<AP: AuthenticationProvider, S: SubscriptionStore, CS: CalendarStore, BS: CalendarStore> Clone
|
||||
for PrincipalResourceService<AP, S, CS, BS>
|
||||
{
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
auth_provider: self.auth_provider.clone(),
|
||||
sub_store: self.sub_store.clone(),
|
||||
cal_store: self.cal_store.clone(),
|
||||
birthday_store: self.birthday_store.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<AP: AuthenticationProvider, S: SubscriptionStore, CS: CalendarStore, BS: CalendarStore>
|
||||
ResourceService for PrincipalResourceService<AP, S, CS, BS>
|
||||
{
|
||||
type PathComponents = (String,);
|
||||
type MemberType = CalendarSetResource;
|
||||
type Resource = PrincipalResource;
|
||||
type Error = Error;
|
||||
type Principal = User;
|
||||
type PrincipalUri = CalDavPrincipalUri;
|
||||
|
||||
const DAV_HEADER: &str = "1, 3, access-control, calendar-access";
|
||||
|
||||
async fn get_resource(
|
||||
&self,
|
||||
(principal,): &Self::PathComponents,
|
||||
) -> Result<Self::Resource, Self::Error> {
|
||||
let user = self
|
||||
.auth_provider
|
||||
.get_principal(principal)
|
||||
.await?
|
||||
.ok_or(crate::Error::NotFound)?;
|
||||
Ok(PrincipalResource {
|
||||
principal: user,
|
||||
home_set: &["calendar", "birthdays"],
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_members(
|
||||
&self,
|
||||
(principal,): &Self::PathComponents,
|
||||
) -> Result<Vec<Self::MemberType>, Self::Error> {
|
||||
Ok(vec![
|
||||
CalendarSetResource {
|
||||
name: "calendar",
|
||||
principal: principal.to_owned(),
|
||||
read_only: false,
|
||||
},
|
||||
CalendarSetResource {
|
||||
name: "birthdays",
|
||||
principal: principal.to_owned(),
|
||||
read_only: true,
|
||||
},
|
||||
])
|
||||
}
|
||||
|
||||
fn axum_router<State: Send + Sync + Clone + 'static>(self) -> axum::Router<State> {
|
||||
Router::new()
|
||||
.nest(
|
||||
"/calendar",
|
||||
CalendarSetResourceService::new(
|
||||
"calendar",
|
||||
self.cal_store.clone(),
|
||||
self.sub_store.clone(),
|
||||
)
|
||||
.axum_router(),
|
||||
)
|
||||
.nest(
|
||||
"/birthdays",
|
||||
CalendarSetResourceService::new(
|
||||
"birthdays",
|
||||
self.birthday_store.clone(),
|
||||
self.sub_store.clone(),
|
||||
)
|
||||
.axum_router(),
|
||||
)
|
||||
.route_service("/", self.axum_service())
|
||||
}
|
||||
}
|
||||
|
||||
impl<AP: AuthenticationProvider, S: SubscriptionStore, CS: CalendarStore, BS: CalendarStore>
|
||||
AxumMethods for PrincipalResourceService<AP, S, CS, BS>
|
||||
{
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::{
|
||||
web::{self, Data, Path},
|
||||
HttpResponse,
|
||||
web::{self, Data, Path},
|
||||
};
|
||||
use rustical_dav::xml::multistatus::PropstatElement;
|
||||
use rustical_store::SubscriptionStore;
|
||||
@@ -17,8 +19,9 @@ async fn handle_delete<S: SubscriptionStore>(
|
||||
Ok(HttpResponse::NoContent().body("Unregistered"))
|
||||
}
|
||||
|
||||
pub fn subscription_resource<S: SubscriptionStore>() -> actix_web::Resource {
|
||||
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>)
|
||||
}
|
||||
|
||||
@@ -7,15 +7,15 @@ repository.workspace = true
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
actix-web = { workspace = true }
|
||||
axum.workspace = true
|
||||
axum-extra.workspace = true
|
||||
tower.workspace = true
|
||||
async-trait = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
quick-xml = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-actix-web = { workspace = true }
|
||||
futures-util = { workspace = true }
|
||||
derive_more = { workspace = true }
|
||||
actix-web-httpauth = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
@@ -26,3 +26,8 @@ chrono = { workspace = true }
|
||||
rustical_xml.workspace = true
|
||||
uuid.workspace = true
|
||||
rustical_dav_push.workspace = true
|
||||
rustical_ical.workspace = true
|
||||
http.workspace = true
|
||||
tower-http.workspace = true
|
||||
percent-encoding.workspace = true
|
||||
ical.workspace = true
|
||||
|
||||
@@ -1,36 +1,36 @@
|
||||
use super::resource::AddressObjectPathComponents;
|
||||
use super::AddressObjectPathComponents;
|
||||
use super::AddressObjectResourceService;
|
||||
use crate::Error;
|
||||
use crate::addressbook::resource::AddressbookResource;
|
||||
use actix_web::HttpRequest;
|
||||
use actix_web::HttpResponse;
|
||||
use actix_web::http::header;
|
||||
use actix_web::http::header::HeaderValue;
|
||||
use actix_web::web::{Data, Path};
|
||||
use axum::body::Body;
|
||||
use axum::extract::{Path, State};
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use axum_extra::TypedHeader;
|
||||
use axum_extra::headers::{ContentType, ETag, HeaderMapExt, IfNoneMatch};
|
||||
use http::{HeaderMap, StatusCode};
|
||||
use rustical_dav::privileges::UserPrivilege;
|
||||
use rustical_dav::resource::Resource;
|
||||
use rustical_ical::AddressObject;
|
||||
use rustical_store::AddressbookStore;
|
||||
use rustical_store::auth::User;
|
||||
use rustical_store::{AddressObject, AddressbookStore};
|
||||
use std::str::FromStr;
|
||||
use tracing::instrument;
|
||||
use tracing_actix_web::RootSpan;
|
||||
|
||||
#[instrument(parent = root_span.id(), skip(store, root_span))]
|
||||
#[instrument(skip(addr_store))]
|
||||
pub async fn get_object<AS: AddressbookStore>(
|
||||
path: Path<AddressObjectPathComponents>,
|
||||
store: Data<AS>,
|
||||
user: User,
|
||||
root_span: RootSpan,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let AddressObjectPathComponents {
|
||||
Path(AddressObjectPathComponents {
|
||||
principal,
|
||||
addressbook_id,
|
||||
object_id,
|
||||
} = path.into_inner();
|
||||
|
||||
}): Path<AddressObjectPathComponents>,
|
||||
State(AddressObjectResourceService { addr_store }): State<AddressObjectResourceService<AS>>,
|
||||
user: User,
|
||||
) -> Result<Response, Error> {
|
||||
if !user.is_principal(&principal) {
|
||||
return Err(Error::Unauthorized);
|
||||
}
|
||||
|
||||
let addressbook = store
|
||||
let addressbook = addr_store
|
||||
.get_addressbook(&principal, &addressbook_id, false)
|
||||
.await?;
|
||||
let addressbook_resource = AddressbookResource(addressbook);
|
||||
@@ -41,42 +41,49 @@ pub async fn get_object<AS: AddressbookStore>(
|
||||
return Err(Error::Unauthorized);
|
||||
}
|
||||
|
||||
let object = store
|
||||
let object = addr_store
|
||||
.get_object(&principal, &addressbook_id, &object_id, false)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Ok()
|
||||
.insert_header(("ETag", object.get_etag()))
|
||||
.insert_header(("Content-Type", "text/vcard"))
|
||||
.body(object.get_vcf().to_owned()))
|
||||
let mut resp = Response::builder().status(StatusCode::OK);
|
||||
let hdrs = resp.headers_mut().unwrap();
|
||||
hdrs.typed_insert(ETag::from_str(&object.get_etag()).unwrap());
|
||||
hdrs.typed_insert(ContentType::from_str("text/vcard").unwrap());
|
||||
Ok(resp.body(Body::new(object.get_vcf().to_owned())).unwrap())
|
||||
}
|
||||
|
||||
#[instrument(parent = root_span.id(), skip(store, req, root_span))]
|
||||
#[instrument(skip(addr_store, body))]
|
||||
pub async fn put_object<AS: AddressbookStore>(
|
||||
path: Path<AddressObjectPathComponents>,
|
||||
store: Data<AS>,
|
||||
body: String,
|
||||
user: User,
|
||||
req: HttpRequest,
|
||||
root_span: RootSpan,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let AddressObjectPathComponents {
|
||||
Path(AddressObjectPathComponents {
|
||||
principal,
|
||||
addressbook_id,
|
||||
object_id,
|
||||
} = path.into_inner();
|
||||
|
||||
}): Path<AddressObjectPathComponents>,
|
||||
State(AddressObjectResourceService { addr_store }): State<AddressObjectResourceService<AS>>,
|
||||
user: User,
|
||||
mut if_none_match: Option<TypedHeader<IfNoneMatch>>,
|
||||
header_map: HeaderMap,
|
||||
body: String,
|
||||
) -> Result<Response, Error> {
|
||||
if !user.is_principal(&principal) {
|
||||
return Err(Error::Unauthorized);
|
||||
}
|
||||
|
||||
let overwrite =
|
||||
Some(&HeaderValue::from_static("*")) != req.headers().get(header::IF_NONE_MATCH);
|
||||
// https://github.com/hyperium/headers/issues/204
|
||||
if !header_map.contains_key("If-None-Match") {
|
||||
if_none_match = None;
|
||||
}
|
||||
|
||||
let overwrite = if let Some(TypedHeader(if_none_match)) = if_none_match {
|
||||
if_none_match == IfNoneMatch::any()
|
||||
} else {
|
||||
true
|
||||
};
|
||||
|
||||
let object = AddressObject::from_vcf(object_id, body)?;
|
||||
store
|
||||
addr_store
|
||||
.put_object(principal, addressbook_id, object, overwrite)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Created().finish())
|
||||
Ok(StatusCode::CREATED.into_response())
|
||||
}
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
pub mod methods;
|
||||
pub mod resource;
|
||||
mod service;
|
||||
pub use service::*;
|
||||
mod prop;
|
||||
pub use prop::*;
|
||||
|
||||
23
crates/carddav/src/address_object/prop.rs
Normal file
23
crates/carddav/src/address_object/prop.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
use rustical_dav::extensions::CommonPropertiesProp;
|
||||
use rustical_xml::{EnumVariants, PropName, XmlDeserialize, XmlSerialize};
|
||||
|
||||
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, PropName)]
|
||||
#[xml(unit_variants_ident = "AddressObjectPropName")]
|
||||
pub enum AddressObjectProp {
|
||||
// WebDAV (RFC 2518)
|
||||
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
|
||||
Getetag(String),
|
||||
#[xml(ns = "rustical_dav::namespace::NS_DAV", skip_deserializing)]
|
||||
Getcontenttype(&'static str),
|
||||
|
||||
// CalDAV (RFC 4791)
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV")]
|
||||
AddressData(String),
|
||||
}
|
||||
|
||||
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, PropName)]
|
||||
#[xml(unit_variants_ident = "AddressObjectPropWrapperName", untagged)]
|
||||
pub enum AddressObjectPropWrapper {
|
||||
AddressObject(AddressObjectProp),
|
||||
Common(CommonPropertiesProp),
|
||||
}
|
||||
@@ -1,45 +1,19 @@
|
||||
use crate::{Error, principal::PrincipalResource};
|
||||
use actix_web::dev::ResourceMap;
|
||||
use async_trait::async_trait;
|
||||
use derive_more::derive::{Constructor, From, Into};
|
||||
use crate::{
|
||||
Error,
|
||||
address_object::{
|
||||
AddressObjectProp, AddressObjectPropName, AddressObjectPropWrapper,
|
||||
AddressObjectPropWrapperName,
|
||||
},
|
||||
};
|
||||
use derive_more::derive::{From, Into};
|
||||
use rustical_dav::{
|
||||
extensions::{CommonPropertiesExtension, CommonPropertiesProp},
|
||||
extensions::CommonPropertiesExtension,
|
||||
privileges::UserPrivilegeSet,
|
||||
resource::{Resource, ResourceService},
|
||||
resource::{PrincipalUri, Resource, ResourceName},
|
||||
xml::Resourcetype,
|
||||
};
|
||||
use rustical_store::{AddressObject, AddressbookStore, auth::User};
|
||||
use rustical_xml::{EnumUnitVariants, EnumVariants, XmlDeserialize, XmlSerialize};
|
||||
use serde::Deserialize;
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::methods::{get_object, put_object};
|
||||
|
||||
#[derive(Constructor)]
|
||||
pub struct AddressObjectResourceService<AS: AddressbookStore> {
|
||||
addr_store: Arc<AS>,
|
||||
}
|
||||
|
||||
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, EnumUnitVariants)]
|
||||
#[xml(unit_variants_ident = "AddressObjectPropName")]
|
||||
pub enum AddressObjectProp {
|
||||
// WebDAV (RFC 2518)
|
||||
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
|
||||
Getetag(String),
|
||||
#[xml(ns = "rustical_dav::namespace::NS_DAV", skip_deserializing)]
|
||||
Getcontenttype(&'static str),
|
||||
|
||||
// CalDAV (RFC 4791)
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV")]
|
||||
AddressData(String),
|
||||
}
|
||||
|
||||
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, EnumUnitVariants)]
|
||||
#[xml(unit_variants_ident = "AddressObjectPropWrapperName", untagged)]
|
||||
pub enum AddressObjectPropWrapper {
|
||||
AddressObject(AddressObjectProp),
|
||||
Common(CommonPropertiesProp),
|
||||
}
|
||||
use rustical_ical::AddressObject;
|
||||
use rustical_store::auth::User;
|
||||
|
||||
#[derive(Clone, From, Into)]
|
||||
pub struct AddressObjectResource {
|
||||
@@ -47,8 +21,10 @@ pub struct AddressObjectResource {
|
||||
pub principal: String,
|
||||
}
|
||||
|
||||
impl CommonPropertiesExtension for AddressObjectResource {
|
||||
type PrincipalResource = PrincipalResource;
|
||||
impl ResourceName for AddressObjectResource {
|
||||
fn get_name(&self) -> String {
|
||||
format!("{}.vcf", self.object.get_id())
|
||||
}
|
||||
}
|
||||
|
||||
impl Resource for AddressObjectResource {
|
||||
@@ -56,13 +32,15 @@ impl Resource for AddressObjectResource {
|
||||
type Error = Error;
|
||||
type Principal = User;
|
||||
|
||||
const IS_COLLECTION: bool = false;
|
||||
|
||||
fn get_resourcetype(&self) -> Resourcetype {
|
||||
Resourcetype(&[])
|
||||
}
|
||||
|
||||
fn get_prop(
|
||||
&self,
|
||||
rmap: &ResourceMap,
|
||||
puri: &impl PrincipalUri,
|
||||
user: &User,
|
||||
prop: &AddressObjectPropWrapperName,
|
||||
) -> Result<Self::Prop, Self::Error> {
|
||||
@@ -81,7 +59,7 @@ impl Resource for AddressObjectResource {
|
||||
})
|
||||
}
|
||||
AddressObjectPropWrapperName::Common(prop) => AddressObjectPropWrapper::Common(
|
||||
CommonPropertiesExtension::get_prop(self, rmap, user, prop)?,
|
||||
CommonPropertiesExtension::get_prop(self, puri, user, prop)?,
|
||||
),
|
||||
})
|
||||
}
|
||||
@@ -100,57 +78,3 @@ impl Resource for AddressObjectResource {
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct AddressObjectPathComponents {
|
||||
pub principal: String,
|
||||
pub addressbook_id: String,
|
||||
pub object_id: String,
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl<AS: AddressbookStore> ResourceService for AddressObjectResourceService<AS> {
|
||||
type PathComponents = AddressObjectPathComponents;
|
||||
type Resource = AddressObjectResource;
|
||||
type MemberType = AddressObjectResource;
|
||||
type Error = Error;
|
||||
type Principal = User;
|
||||
|
||||
async fn get_resource(
|
||||
&self,
|
||||
AddressObjectPathComponents {
|
||||
principal,
|
||||
addressbook_id,
|
||||
object_id,
|
||||
}: &Self::PathComponents,
|
||||
) -> Result<Self::Resource, Self::Error> {
|
||||
let object = self
|
||||
.addr_store
|
||||
.get_object(principal, addressbook_id, object_id, false)
|
||||
.await?;
|
||||
Ok(AddressObjectResource {
|
||||
object,
|
||||
principal: principal.to_owned(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn delete_resource(
|
||||
&self,
|
||||
AddressObjectPathComponents {
|
||||
principal,
|
||||
addressbook_id,
|
||||
object_id,
|
||||
}: &Self::PathComponents,
|
||||
use_trashbin: bool,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.addr_store
|
||||
.delete_object(principal, addressbook_id, object_id, use_trashbin)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn actix_additional_routes(res: actix_web::Resource) -> actix_web::Resource {
|
||||
res.get(get_object::<AS>).put(put_object::<AS>)
|
||||
}
|
||||
}
|
||||
|
||||
105
crates/carddav/src/address_object/service.rs
Normal file
105
crates/carddav/src/address_object/service.rs
Normal file
@@ -0,0 +1,105 @@
|
||||
use super::methods::{get_object, put_object};
|
||||
use crate::{CardDavPrincipalUri, Error, address_object::resource::AddressObjectResource};
|
||||
use async_trait::async_trait;
|
||||
use axum::{extract::Request, handler::Handler, response::Response};
|
||||
use derive_more::derive::Constructor;
|
||||
use futures_util::future::BoxFuture;
|
||||
use rustical_dav::resource::{AxumMethods, ResourceService};
|
||||
use rustical_store::{AddressbookStore, auth::User};
|
||||
use serde::{Deserialize, Deserializer};
|
||||
use std::{convert::Infallible, sync::Arc};
|
||||
use tower::Service;
|
||||
|
||||
#[derive(Constructor)]
|
||||
pub struct AddressObjectResourceService<AS: AddressbookStore> {
|
||||
pub(crate) addr_store: Arc<AS>,
|
||||
}
|
||||
|
||||
impl<AS: AddressbookStore> Clone for AddressObjectResourceService<AS> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
addr_store: self.addr_store.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct AddressObjectPathComponents {
|
||||
pub principal: String,
|
||||
pub addressbook_id: String,
|
||||
#[serde(deserialize_with = "deserialize_vcf_name")]
|
||||
pub object_id: String,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<AS: AddressbookStore> ResourceService for AddressObjectResourceService<AS> {
|
||||
type PathComponents = AddressObjectPathComponents;
|
||||
type Resource = AddressObjectResource;
|
||||
type MemberType = AddressObjectResource;
|
||||
type Error = Error;
|
||||
type Principal = User;
|
||||
type PrincipalUri = CardDavPrincipalUri;
|
||||
|
||||
const DAV_HEADER: &str = "1, 3, access-control, addressbook";
|
||||
|
||||
async fn get_resource(
|
||||
&self,
|
||||
AddressObjectPathComponents {
|
||||
principal,
|
||||
addressbook_id,
|
||||
object_id,
|
||||
}: &Self::PathComponents,
|
||||
) -> Result<Self::Resource, Self::Error> {
|
||||
let object = self
|
||||
.addr_store
|
||||
.get_object(principal, addressbook_id, object_id, false)
|
||||
.await?;
|
||||
Ok(AddressObjectResource {
|
||||
object,
|
||||
principal: principal.to_owned(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn delete_resource(
|
||||
&self,
|
||||
AddressObjectPathComponents {
|
||||
principal,
|
||||
addressbook_id,
|
||||
object_id,
|
||||
}: &Self::PathComponents,
|
||||
use_trashbin: bool,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.addr_store
|
||||
.delete_object(principal, addressbook_id, object_id, use_trashbin)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<AS: AddressbookStore> AxumMethods for AddressObjectResourceService<AS> {
|
||||
fn get() -> Option<fn(Self, Request) -> BoxFuture<'static, Result<Response, Infallible>>> {
|
||||
Some(|state, req| {
|
||||
let mut service = Handler::with_state(get_object::<AS>, state);
|
||||
Box::pin(Service::call(&mut service, req))
|
||||
})
|
||||
}
|
||||
|
||||
fn put() -> Option<fn(Self, Request) -> BoxFuture<'static, Result<Response, Infallible>>> {
|
||||
Some(|state, req| {
|
||||
let mut service = Handler::with_state(put_object::<AS>, state);
|
||||
Box::pin(Service::call(&mut service, req))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_vcf_name<'de, D>(deserializer: D) -> Result<String, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let name: String = Deserialize::deserialize(deserializer)?;
|
||||
if let Some(object_id) = name.strip_suffix(".vcf") {
|
||||
Ok(object_id.to_owned())
|
||||
} else {
|
||||
Err(serde::de::Error::custom("Missing .vcf extension"))
|
||||
}
|
||||
}
|
||||
59
crates/carddav/src/addressbook/methods/get.rs
Normal file
59
crates/carddav/src/addressbook/methods/get.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
use crate::Error;
|
||||
use crate::addressbook::AddressbookResourceService;
|
||||
use crate::addressbook::resource::AddressbookResource;
|
||||
use axum::body::Body;
|
||||
use axum::extract::{Path, State};
|
||||
use axum::response::Response;
|
||||
use axum_extra::headers::{ContentType, HeaderMapExt};
|
||||
use http::{HeaderValue, StatusCode, header};
|
||||
use percent_encoding::{CONTROLS, utf8_percent_encode};
|
||||
use rustical_dav::privileges::UserPrivilege;
|
||||
use rustical_dav::resource::Resource;
|
||||
use rustical_ical::AddressObject;
|
||||
use rustical_store::auth::User;
|
||||
use rustical_store::{AddressbookStore, SubscriptionStore};
|
||||
use std::str::FromStr;
|
||||
use tracing::instrument;
|
||||
|
||||
#[instrument(skip(addr_store))]
|
||||
pub async fn route_get<AS: AddressbookStore, S: SubscriptionStore>(
|
||||
Path((principal, addressbook_id)): Path<(String, String)>,
|
||||
State(AddressbookResourceService { addr_store, .. }): State<AddressbookResourceService<AS, S>>,
|
||||
user: User,
|
||||
) -> Result<Response, Error> {
|
||||
if !user.is_principal(&principal) {
|
||||
return Err(Error::Unauthorized);
|
||||
}
|
||||
|
||||
let addressbook = addr_store
|
||||
.get_addressbook(&principal, &addressbook_id, false)
|
||||
.await?;
|
||||
let addressbook_resource = AddressbookResource(addressbook);
|
||||
if !addressbook_resource
|
||||
.get_user_privileges(&user)?
|
||||
.has(&UserPrivilege::Read)
|
||||
{
|
||||
return Err(Error::Unauthorized);
|
||||
}
|
||||
|
||||
let objects = addr_store.get_objects(&principal, &addressbook_id).await?;
|
||||
let vcf = objects
|
||||
.iter()
|
||||
.map(AddressObject::get_vcf)
|
||||
.collect::<Vec<_>>()
|
||||
.join("\r\n");
|
||||
|
||||
let mut resp = Response::builder().status(StatusCode::OK);
|
||||
let hdrs = resp.headers_mut().unwrap();
|
||||
hdrs.typed_insert(ContentType::from_str("text/vcard").unwrap());
|
||||
let filename = format!("{}_{}.vcf", principal, addressbook_id);
|
||||
let filename = utf8_percent_encode(&filename, CONTROLS);
|
||||
hdrs.insert(
|
||||
header::CONTENT_DISPOSITION,
|
||||
HeaderValue::from_str(&format!(
|
||||
"attachement; filename*=UTF-8''{filename}; filename={filename}",
|
||||
))
|
||||
.unwrap(),
|
||||
);
|
||||
Ok(resp.body(Body::new(vcf)).unwrap())
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
use crate::Error;
|
||||
use actix_web::web::Path;
|
||||
use actix_web::{HttpResponse, web::Data};
|
||||
use rustical_store::{Addressbook, AddressbookStore, auth::User};
|
||||
use crate::{Error, addressbook::AddressbookResourceService};
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use http::StatusCode;
|
||||
use rustical_store::{Addressbook, AddressbookStore, SubscriptionStore, auth::User};
|
||||
use rustical_xml::{XmlDeserialize, XmlDocument, XmlRootTag};
|
||||
use tracing::instrument;
|
||||
use tracing_actix_web::RootSpan;
|
||||
|
||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
|
||||
pub struct Resourcetype {
|
||||
@@ -39,15 +41,13 @@ struct MkcolRequest {
|
||||
set: PropElement<MkcolAddressbookProp>,
|
||||
}
|
||||
|
||||
#[instrument(parent = root_span.id(), skip(store, root_span))]
|
||||
pub async fn route_mkcol<AS: AddressbookStore>(
|
||||
path: Path<(String, String)>,
|
||||
body: String,
|
||||
#[instrument(skip(addr_store))]
|
||||
pub async fn route_mkcol<AS: AddressbookStore, S: SubscriptionStore>(
|
||||
Path((principal, addressbook_id)): Path<(String, String)>,
|
||||
user: User,
|
||||
store: Data<AS>,
|
||||
root_span: RootSpan,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let (principal, addressbook_id) = path.into_inner();
|
||||
State(AddressbookResourceService { addr_store, .. }): State<AddressbookResourceService<AS, S>>,
|
||||
body: String,
|
||||
) -> Result<Response, Error> {
|
||||
if !user.is_principal(&principal) {
|
||||
return Err(Error::Unauthorized);
|
||||
}
|
||||
@@ -65,7 +65,7 @@ pub async fn route_mkcol<AS: AddressbookStore>(
|
||||
push_topic: uuid::Uuid::new_v4().to_string(),
|
||||
};
|
||||
|
||||
match store
|
||||
match addr_store
|
||||
.get_addressbook(&principal, &addressbook_id, true)
|
||||
.await
|
||||
{
|
||||
@@ -74,7 +74,11 @@ pub async fn route_mkcol<AS: AddressbookStore>(
|
||||
}
|
||||
Ok(_) => {
|
||||
// oh no, there's a conflict
|
||||
return Ok(HttpResponse::Conflict().body("An addressbook already exists at this URI"));
|
||||
return Ok((
|
||||
StatusCode::CONFLICT,
|
||||
"An addressbook already exists at this URI",
|
||||
)
|
||||
.into_response());
|
||||
}
|
||||
Err(err) => {
|
||||
// some other error
|
||||
@@ -82,12 +86,10 @@ pub async fn route_mkcol<AS: AddressbookStore>(
|
||||
}
|
||||
}
|
||||
|
||||
match store.insert_addressbook(addressbook).await {
|
||||
match addr_store.insert_addressbook(addressbook).await {
|
||||
// TODO: The spec says we should return a mkcol-response.
|
||||
// However, it works without one but breaks on iPadOS when using an empty one :)
|
||||
Ok(()) => Ok(HttpResponse::Created()
|
||||
.insert_header(("Cache-Control", "no-cache"))
|
||||
.body("")),
|
||||
Ok(()) => Ok(StatusCode::CREATED.into_response()),
|
||||
Err(err) => {
|
||||
dbg!(err.to_string());
|
||||
Err(err.into())
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
pub mod mkcol;
|
||||
pub mod post;
|
||||
// pub mod post;
|
||||
pub mod get;
|
||||
pub mod put;
|
||||
pub mod report;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::Error;
|
||||
use crate::addressbook::resource::AddressbookResourceService;
|
||||
use actix_web::http::header;
|
||||
use actix_web::web::{Data, Path};
|
||||
use actix_web::{HttpRequest, HttpResponse};
|
||||
@@ -9,13 +10,12 @@ use rustical_xml::XmlDocument;
|
||||
use tracing::instrument;
|
||||
use tracing_actix_web::RootSpan;
|
||||
|
||||
#[instrument(parent = root_span.id(), skip(store, subscription_store, root_span, req))]
|
||||
#[instrument(parent = root_span.id(), skip(resource_service, root_span, req))]
|
||||
pub async fn route_post<A: AddressbookStore, S: SubscriptionStore>(
|
||||
path: Path<(String, String)>,
|
||||
body: String,
|
||||
user: User,
|
||||
store: Data<A>,
|
||||
subscription_store: Data<S>,
|
||||
resource_service: Data<AddressbookResourceService<A, S>>,
|
||||
root_span: RootSpan,
|
||||
req: HttpRequest,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
@@ -24,7 +24,8 @@ pub async fn route_post<A: AddressbookStore, S: SubscriptionStore>(
|
||||
return Err(Error::Unauthorized);
|
||||
}
|
||||
|
||||
let addressbook = store
|
||||
let addressbook = resource_service
|
||||
.addr_store
|
||||
.get_addressbook(&principal, &addressbook_id, false)
|
||||
.await?;
|
||||
let request = PushRegister::parse_str(&body)?;
|
||||
@@ -57,7 +58,10 @@ pub async fn route_post<A: AddressbookStore, S: SubscriptionStore>(
|
||||
.ty,
|
||||
auth_secret: request.subscription.web_push_subscription.auth_secret,
|
||||
};
|
||||
subscription_store.upsert_subscription(subscription).await?;
|
||||
resource_service
|
||||
.sub_store
|
||||
.upsert_subscription(subscription)
|
||||
.await?;
|
||||
|
||||
let location = req
|
||||
.resource_map()
|
||||
|
||||
47
crates/carddav/src/addressbook/methods/put.rs
Normal file
47
crates/carddav/src/addressbook/methods/put.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
use crate::Error;
|
||||
use crate::addressbook::AddressbookResourceService;
|
||||
use axum::response::IntoResponse;
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
response::Response,
|
||||
};
|
||||
use http::StatusCode;
|
||||
use ical::VcardParser;
|
||||
use rustical_ical::AddressObject;
|
||||
use rustical_store::Addressbook;
|
||||
use rustical_store::{AddressbookStore, SubscriptionStore, auth::User};
|
||||
use tracing::instrument;
|
||||
|
||||
#[instrument(skip(addr_store))]
|
||||
pub async fn route_put<AS: AddressbookStore, S: SubscriptionStore>(
|
||||
Path((principal, addressbook_id)): Path<(String, String)>,
|
||||
State(AddressbookResourceService { addr_store, .. }): State<AddressbookResourceService<AS, S>>,
|
||||
user: User,
|
||||
body: String,
|
||||
) -> Result<Response, Error> {
|
||||
if !user.is_principal(&principal) {
|
||||
return Err(Error::Unauthorized);
|
||||
}
|
||||
|
||||
let mut objects = vec![];
|
||||
for object in VcardParser::new(body.as_bytes()) {
|
||||
let object = object.map_err(rustical_ical::Error::from)?;
|
||||
objects.push(AddressObject::try_from(object)?);
|
||||
}
|
||||
|
||||
let addressbook = Addressbook {
|
||||
id: addressbook_id.clone(),
|
||||
principal: principal.clone(),
|
||||
displayname: None,
|
||||
description: None,
|
||||
deleted_at: None,
|
||||
synctoken: Default::default(),
|
||||
push_topic: uuid::Uuid::new_v4().to_string(),
|
||||
};
|
||||
|
||||
addr_store
|
||||
.import_addressbook(principal.clone(), addressbook, objects)
|
||||
.await?;
|
||||
|
||||
Ok(StatusCode::CREATED.into_response())
|
||||
}
|
||||
@@ -1,17 +1,16 @@
|
||||
use crate::{
|
||||
Error,
|
||||
address_object::resource::{AddressObjectPropWrapper, AddressObjectResource},
|
||||
};
|
||||
use actix_web::{
|
||||
HttpRequest,
|
||||
dev::{Path, ResourceDef},
|
||||
http::StatusCode,
|
||||
address_object::{
|
||||
AddressObjectPropWrapper, AddressObjectPropWrapperName, resource::AddressObjectResource,
|
||||
},
|
||||
};
|
||||
use http::StatusCode;
|
||||
use rustical_dav::{
|
||||
resource::Resource,
|
||||
resource::{PrincipalUri, Resource},
|
||||
xml::{MultistatusElement, PropfindType, multistatus::ResponseElement},
|
||||
};
|
||||
use rustical_store::{AddressObject, AddressbookStore, auth::User};
|
||||
use rustical_ical::AddressObject;
|
||||
use rustical_store::{AddressbookStore, auth::User};
|
||||
use rustical_xml::XmlDeserialize;
|
||||
|
||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
|
||||
@@ -19,7 +18,7 @@ use rustical_xml::XmlDeserialize;
|
||||
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
|
||||
pub struct AddressbookMultigetRequest {
|
||||
#[xml(ns = "rustical_dav::namespace::NS_DAV", ty = "untagged")]
|
||||
pub(crate) prop: PropfindType,
|
||||
pub(crate) prop: PropfindType<AddressObjectPropWrapperName>,
|
||||
#[xml(ns = "rustical_dav::namespace::NS_DAV", flatten)]
|
||||
pub(crate) href: Vec<String>,
|
||||
}
|
||||
@@ -31,27 +30,29 @@ pub async fn get_objects_addressbook_multiget<AS: AddressbookStore>(
|
||||
addressbook_id: &str,
|
||||
store: &AS,
|
||||
) -> Result<(Vec<AddressObject>, Vec<String>), Error> {
|
||||
let resource_def = ResourceDef::prefix(path).join(&ResourceDef::new("/{object_id}.vcf"));
|
||||
|
||||
let mut result = vec![];
|
||||
let mut not_found = vec![];
|
||||
|
||||
for href in &addressbook_multiget.href {
|
||||
let mut path = Path::new(href.as_str());
|
||||
if !resource_def.capture_match_info(&mut path) {
|
||||
if let Some(filename) = href.strip_prefix(path) {
|
||||
let filename = filename.trim_start_matches("/");
|
||||
if let Some(object_id) = filename.strip_suffix(".vcf") {
|
||||
match store
|
||||
.get_object(principal, addressbook_id, object_id, false)
|
||||
.await
|
||||
{
|
||||
Ok(object) => result.push(object),
|
||||
Err(rustical_store::Error::NotFound) => not_found.push(href.to_owned()),
|
||||
Err(err) => return Err(err.into()),
|
||||
};
|
||||
} else {
|
||||
not_found.push(href.to_owned());
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
not_found.push(href.to_owned());
|
||||
continue;
|
||||
};
|
||||
let object_id = path.get("object_id").unwrap();
|
||||
match store
|
||||
.get_object(principal, addressbook_id, object_id, false)
|
||||
.await
|
||||
{
|
||||
Ok(object) => result.push(object),
|
||||
Err(rustical_store::Error::NotFound) => not_found.push(href.to_owned()),
|
||||
// TODO: Maybe add error handling on a per-object basis
|
||||
Err(err) => return Err(err.into()),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Ok((result, not_found))
|
||||
@@ -59,26 +60,27 @@ pub async fn get_objects_addressbook_multiget<AS: AddressbookStore>(
|
||||
|
||||
pub async fn handle_addressbook_multiget<AS: AddressbookStore>(
|
||||
addr_multiget: &AddressbookMultigetRequest,
|
||||
props: &[&str],
|
||||
req: HttpRequest,
|
||||
prop: &PropfindType<AddressObjectPropWrapperName>,
|
||||
path: &str,
|
||||
puri: &impl PrincipalUri,
|
||||
user: &User,
|
||||
principal: &str,
|
||||
cal_id: &str,
|
||||
addr_store: &AS,
|
||||
) -> Result<MultistatusElement<AddressObjectPropWrapper, String>, Error> {
|
||||
let (objects, not_found) =
|
||||
get_objects_addressbook_multiget(addr_multiget, req.path(), principal, cal_id, addr_store)
|
||||
get_objects_addressbook_multiget(addr_multiget, path, principal, cal_id, addr_store)
|
||||
.await?;
|
||||
|
||||
let mut responses = Vec::new();
|
||||
for object in objects {
|
||||
let path = format!("{}/{}.vcf", req.path(), object.get_id());
|
||||
let path = format!("{}/{}.vcf", path, object.get_id());
|
||||
responses.push(
|
||||
AddressObjectResource {
|
||||
object,
|
||||
principal: principal.to_owned(),
|
||||
}
|
||||
.propfind(&path, props, user, req.resource_map())?,
|
||||
.propfind(&path, prop, puri, user)?,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
use crate::Error;
|
||||
use actix_web::{
|
||||
HttpRequest, Responder,
|
||||
web::{Data, Path},
|
||||
use crate::{
|
||||
CardDavPrincipalUri, Error, address_object::AddressObjectPropWrapperName,
|
||||
addressbook::AddressbookResourceService,
|
||||
};
|
||||
use addressbook_multiget::{AddressbookMultigetRequest, handle_addressbook_multiget};
|
||||
use rustical_dav::xml::{PropElement, PropfindType, sync_collection::SyncCollectionRequest};
|
||||
use rustical_store::{AddressbookStore, auth::User};
|
||||
use axum::{
|
||||
Extension,
|
||||
extract::{OriginalUri, Path, State},
|
||||
response::IntoResponse,
|
||||
};
|
||||
use rustical_dav::xml::{PropfindType, sync_collection::SyncCollectionRequest};
|
||||
use rustical_store::{AddressbookStore, SubscriptionStore, auth::User};
|
||||
use rustical_xml::{XmlDeserialize, XmlDocument};
|
||||
use sync_collection::handle_sync_collection;
|
||||
use tracing::instrument;
|
||||
@@ -18,53 +22,40 @@ pub(crate) enum ReportRequest {
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV")]
|
||||
AddressbookMultiget(AddressbookMultigetRequest),
|
||||
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
|
||||
SyncCollection(SyncCollectionRequest),
|
||||
SyncCollection(SyncCollectionRequest<AddressObjectPropWrapperName>),
|
||||
}
|
||||
|
||||
impl ReportRequest {
|
||||
fn props(&self) -> Vec<&str> {
|
||||
let prop_element = match self {
|
||||
fn props(&self) -> &PropfindType<AddressObjectPropWrapperName> {
|
||||
match self {
|
||||
ReportRequest::AddressbookMultiget(AddressbookMultigetRequest { prop, .. }) => prop,
|
||||
ReportRequest::SyncCollection(SyncCollectionRequest { prop, .. }) => prop,
|
||||
};
|
||||
|
||||
match prop_element {
|
||||
PropfindType::Allprop => {
|
||||
vec!["allprop"]
|
||||
}
|
||||
PropfindType::Propname => {
|
||||
vec!["propname"]
|
||||
}
|
||||
PropfindType::Prop(PropElement(prop_tags)) => prop_tags
|
||||
.iter()
|
||||
.map(|propname| propname.0.as_str())
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(req, addr_store))]
|
||||
pub async fn route_report_addressbook<AS: AddressbookStore>(
|
||||
path: Path<(String, String)>,
|
||||
body: String,
|
||||
#[instrument(skip(addr_store))]
|
||||
pub async fn route_report_addressbook<AS: AddressbookStore, S: SubscriptionStore>(
|
||||
Path((principal, addressbook_id)): Path<(String, String)>,
|
||||
user: User,
|
||||
req: HttpRequest,
|
||||
addr_store: Data<AS>,
|
||||
) -> Result<impl Responder, Error> {
|
||||
let (principal, addressbook_id) = path.into_inner();
|
||||
OriginalUri(uri): OriginalUri,
|
||||
Extension(puri): Extension<CardDavPrincipalUri>,
|
||||
State(AddressbookResourceService { addr_store, .. }): State<AddressbookResourceService<AS, S>>,
|
||||
body: String,
|
||||
) -> Result<impl IntoResponse, Error> {
|
||||
if !user.is_principal(&principal) {
|
||||
return Err(Error::Unauthorized);
|
||||
}
|
||||
|
||||
let request = ReportRequest::parse_str(&body)?;
|
||||
let props = request.props();
|
||||
|
||||
Ok(match &request {
|
||||
ReportRequest::AddressbookMultiget(addr_multiget) => {
|
||||
handle_addressbook_multiget(
|
||||
addr_multiget,
|
||||
&props,
|
||||
req,
|
||||
request.props(),
|
||||
uri.path(),
|
||||
&puri,
|
||||
&user,
|
||||
&principal,
|
||||
&addressbook_id,
|
||||
@@ -75,8 +66,8 @@ pub async fn route_report_addressbook<AS: AddressbookStore>(
|
||||
ReportRequest::SyncCollection(sync_collection) => {
|
||||
handle_sync_collection(
|
||||
sync_collection,
|
||||
&props,
|
||||
req,
|
||||
uri.path(),
|
||||
&puri,
|
||||
&user,
|
||||
&principal,
|
||||
&addressbook_id,
|
||||
@@ -89,9 +80,9 @@ pub async fn route_report_addressbook<AS: AddressbookStore>(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rustical_dav::xml::{PropElement, Propname, sync_collection::SyncLevel};
|
||||
|
||||
use super::*;
|
||||
use crate::address_object::AddressObjectPropName;
|
||||
use rustical_dav::xml::{PropElement, sync_collection::SyncLevel};
|
||||
|
||||
#[test]
|
||||
fn test_xml_sync_collection() {
|
||||
@@ -112,9 +103,12 @@ mod tests {
|
||||
ReportRequest::SyncCollection(SyncCollectionRequest {
|
||||
sync_token: "".to_owned(),
|
||||
sync_level: SyncLevel::One,
|
||||
prop: rustical_dav::xml::PropfindType::Prop(PropElement(vec![Propname(
|
||||
"getetag".to_owned()
|
||||
)])),
|
||||
prop: rustical_dav::xml::PropfindType::Prop(PropElement(
|
||||
vec![AddressObjectPropWrapperName::AddressObject(
|
||||
AddressObjectPropName::Getetag
|
||||
)],
|
||||
vec![]
|
||||
)),
|
||||
limit: None
|
||||
})
|
||||
)
|
||||
@@ -137,9 +131,13 @@ mod tests {
|
||||
report_request,
|
||||
ReportRequest::AddressbookMultiget(AddressbookMultigetRequest {
|
||||
prop: rustical_dav::xml::PropfindType::Prop(PropElement(vec![
|
||||
Propname("getetag".to_owned()),
|
||||
Propname("address-data".to_owned())
|
||||
])),
|
||||
AddressObjectPropWrapperName::AddressObject(
|
||||
AddressObjectPropName::Getetag
|
||||
),
|
||||
AddressObjectPropWrapperName::AddressObject(
|
||||
AddressObjectPropName::AddressData
|
||||
),
|
||||
], vec![])),
|
||||
href: vec![
|
||||
"/carddav/user/user/6f787542-5256-401a-8db97003260da/ae7a998fdfd1d84a20391168962c62b".to_owned()
|
||||
]
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
use crate::{
|
||||
Error,
|
||||
address_object::resource::{AddressObjectPropWrapper, AddressObjectResource},
|
||||
address_object::{
|
||||
AddressObjectPropWrapper, AddressObjectPropWrapperName, resource::AddressObjectResource,
|
||||
},
|
||||
};
|
||||
use actix_web::{HttpRequest, http::StatusCode};
|
||||
use http::StatusCode;
|
||||
use rustical_dav::{
|
||||
resource::Resource,
|
||||
resource::{PrincipalUri, Resource},
|
||||
xml::{
|
||||
MultistatusElement, multistatus::ResponseElement, sync_collection::SyncCollectionRequest,
|
||||
},
|
||||
@@ -16,9 +18,9 @@ use rustical_store::{
|
||||
};
|
||||
|
||||
pub async fn handle_sync_collection<AS: AddressbookStore>(
|
||||
sync_collection: &SyncCollectionRequest,
|
||||
props: &[&str],
|
||||
req: HttpRequest,
|
||||
sync_collection: &SyncCollectionRequest<AddressObjectPropWrapperName>,
|
||||
path: &str,
|
||||
puri: &impl PrincipalUri,
|
||||
user: &User,
|
||||
principal: &str,
|
||||
addressbook_id: &str,
|
||||
@@ -31,22 +33,18 @@ pub async fn handle_sync_collection<AS: AddressbookStore>(
|
||||
|
||||
let mut responses = Vec::new();
|
||||
for object in new_objects {
|
||||
let path = format!(
|
||||
"{}/{}.vcf",
|
||||
req.path().trim_end_matches('/'),
|
||||
object.get_id()
|
||||
);
|
||||
let path = format!("{}/{}.vcf", path.trim_end_matches('/'), object.get_id());
|
||||
responses.push(
|
||||
AddressObjectResource {
|
||||
object,
|
||||
principal: principal.to_owned(),
|
||||
}
|
||||
.propfind(&path, props, user, req.resource_map())?,
|
||||
.propfind(&path, &sync_collection.prop, puri, user)?,
|
||||
);
|
||||
}
|
||||
|
||||
for object_id in deleted_objects {
|
||||
let path = format!("{}/{}.vcf", req.path().trim_end_matches('/'), object_id);
|
||||
let path = format!("{}/{}.vcf", path.trim_end_matches('/'), object_id);
|
||||
responses.push(ResponseElement {
|
||||
href: path,
|
||||
status: Some(StatusCode::NOT_FOUND),
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
pub mod methods;
|
||||
pub mod prop;
|
||||
pub mod resource;
|
||||
mod service;
|
||||
pub use service::*;
|
||||
|
||||
@@ -1,4 +1,33 @@
|
||||
use rustical_xml::XmlSerialize;
|
||||
use rustical_dav::extensions::{CommonPropertiesProp, SyncTokenExtensionProp};
|
||||
use rustical_dav_push::DavPushExtensionProp;
|
||||
use rustical_xml::{EnumVariants, PropName, XmlDeserialize, XmlSerialize};
|
||||
|
||||
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, PropName)]
|
||||
#[xml(unit_variants_ident = "AddressbookPropName")]
|
||||
pub enum AddressbookProp {
|
||||
// WebDAV (RFC 2518)
|
||||
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
|
||||
Displayname(Option<String>),
|
||||
|
||||
// CardDAV (RFC 6352)
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV")]
|
||||
AddressbookDescription(Option<String>),
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV", skip_deserializing)]
|
||||
SupportedAddressData(SupportedAddressData),
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV", skip_deserializing)]
|
||||
SupportedReportSet(SupportedReportSet),
|
||||
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
|
||||
MaxResourceSize(i64),
|
||||
}
|
||||
|
||||
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, PropName)]
|
||||
#[xml(unit_variants_ident = "AddressbookPropWrapperName", untagged)]
|
||||
pub enum AddressbookPropWrapper {
|
||||
Addressbook(AddressbookProp),
|
||||
SyncToken(SyncTokenExtensionProp),
|
||||
DavPush(DavPushExtensionProp),
|
||||
Common(CommonPropertiesProp),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, XmlSerialize, PartialEq)]
|
||||
pub struct AddressDataType {
|
||||
|
||||
@@ -1,73 +1,26 @@
|
||||
use super::methods::mkcol::route_mkcol;
|
||||
use super::methods::post::route_post;
|
||||
use super::methods::report::route_report_addressbook;
|
||||
use super::prop::{SupportedAddressData, SupportedReportSet};
|
||||
use crate::Error;
|
||||
use crate::address_object::resource::AddressObjectResource;
|
||||
use crate::principal::PrincipalResource;
|
||||
use actix_web::dev::ResourceMap;
|
||||
use actix_web::http::Method;
|
||||
use actix_web::web;
|
||||
use async_trait::async_trait;
|
||||
use derive_more::derive::{From, Into};
|
||||
use rustical_dav::extensions::{
|
||||
CommonPropertiesExtension, CommonPropertiesProp, SyncTokenExtension, SyncTokenExtensionProp,
|
||||
use crate::addressbook::prop::{
|
||||
AddressbookProp, AddressbookPropName, AddressbookPropWrapper, AddressbookPropWrapperName,
|
||||
};
|
||||
use derive_more::derive::{From, Into};
|
||||
use rustical_dav::extensions::{CommonPropertiesExtension, SyncTokenExtension};
|
||||
use rustical_dav::privileges::UserPrivilegeSet;
|
||||
use rustical_dav::resource::{Resource, ResourceService};
|
||||
use rustical_dav::resource::{PrincipalUri, Resource, ResourceName};
|
||||
use rustical_dav::xml::{Resourcetype, ResourcetypeInner};
|
||||
use rustical_dav_push::{DavPushExtension, DavPushExtensionProp};
|
||||
use rustical_dav_push::DavPushExtension;
|
||||
use rustical_store::Addressbook;
|
||||
use rustical_store::auth::User;
|
||||
use rustical_store::{Addressbook, AddressbookStore, SubscriptionStore};
|
||||
use rustical_xml::{EnumUnitVariants, EnumVariants, XmlDeserialize, XmlSerialize};
|
||||
use std::marker::PhantomData;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct AddressbookResourceService<AS: AddressbookStore, S: SubscriptionStore> {
|
||||
addr_store: Arc<AS>,
|
||||
__phantom_sub: PhantomData<S>,
|
||||
}
|
||||
|
||||
impl<A: AddressbookStore, S: SubscriptionStore> AddressbookResourceService<A, S> {
|
||||
pub fn new(addr_store: Arc<A>) -> Self {
|
||||
Self {
|
||||
addr_store,
|
||||
__phantom_sub: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, EnumUnitVariants)]
|
||||
#[xml(unit_variants_ident = "AddressbookPropName")]
|
||||
pub enum AddressbookProp {
|
||||
// WebDAV (RFC 2518)
|
||||
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
|
||||
Displayname(Option<String>),
|
||||
|
||||
// CardDAV (RFC 6352)
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV")]
|
||||
AddressbookDescription(Option<String>),
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV", skip_deserializing)]
|
||||
SupportedAddressData(SupportedAddressData),
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV", skip_deserializing)]
|
||||
SupportedReportSet(SupportedReportSet),
|
||||
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
|
||||
MaxResourceSize(i64),
|
||||
}
|
||||
|
||||
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, EnumUnitVariants)]
|
||||
#[xml(unit_variants_ident = "AddressbookPropWrapperName", untagged)]
|
||||
pub enum AddressbookPropWrapper {
|
||||
Addressbook(AddressbookProp),
|
||||
SyncToken(SyncTokenExtensionProp),
|
||||
DavPush(DavPushExtensionProp),
|
||||
Common(CommonPropertiesProp),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, From, Into)]
|
||||
pub struct AddressbookResource(pub(crate) Addressbook);
|
||||
|
||||
impl ResourceName for AddressbookResource {
|
||||
fn get_name(&self) -> String {
|
||||
self.0.id.to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
impl SyncTokenExtension for AddressbookResource {
|
||||
fn get_synctoken(&self) -> String {
|
||||
self.0.format_synctoken()
|
||||
@@ -80,15 +33,13 @@ impl DavPushExtension for AddressbookResource {
|
||||
}
|
||||
}
|
||||
|
||||
impl CommonPropertiesExtension for AddressbookResource {
|
||||
type PrincipalResource = PrincipalResource;
|
||||
}
|
||||
|
||||
impl Resource for AddressbookResource {
|
||||
type Prop = AddressbookPropWrapper;
|
||||
type Error = Error;
|
||||
type Principal = User;
|
||||
|
||||
const IS_COLLECTION: bool = true;
|
||||
|
||||
fn get_resourcetype(&self) -> Resourcetype {
|
||||
Resourcetype(&[
|
||||
ResourcetypeInner(Some(rustical_dav::namespace::NS_DAV), "collection"),
|
||||
@@ -98,7 +49,7 @@ impl Resource for AddressbookResource {
|
||||
|
||||
fn get_prop(
|
||||
&self,
|
||||
rmap: &ResourceMap,
|
||||
puri: &impl PrincipalUri,
|
||||
user: &User,
|
||||
prop: &AddressbookPropWrapperName,
|
||||
) -> Result<Self::Prop, Self::Error> {
|
||||
@@ -130,7 +81,7 @@ impl Resource for AddressbookResource {
|
||||
AddressbookPropWrapper::DavPush(<Self as DavPushExtension>::get_prop(self, prop)?)
|
||||
}
|
||||
AddressbookPropWrapperName::Common(prop) => AddressbookPropWrapper::Common(
|
||||
CommonPropertiesExtension::get_prop(self, rmap, user, prop)?,
|
||||
CommonPropertiesExtension::get_prop(self, puri, user, prop)?,
|
||||
),
|
||||
})
|
||||
}
|
||||
@@ -194,78 +145,3 @@ impl Resource for AddressbookResource {
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl<AS: AddressbookStore, S: SubscriptionStore> ResourceService
|
||||
for AddressbookResourceService<AS, S>
|
||||
{
|
||||
type MemberType = AddressObjectResource;
|
||||
type PathComponents = (String, String); // principal, addressbook_id
|
||||
type Resource = AddressbookResource;
|
||||
type Error = Error;
|
||||
type Principal = User;
|
||||
|
||||
async fn get_resource(
|
||||
&self,
|
||||
(principal, addressbook_id): &Self::PathComponents,
|
||||
) -> Result<Self::Resource, Error> {
|
||||
let addressbook = self
|
||||
.addr_store
|
||||
.get_addressbook(principal, addressbook_id, false)
|
||||
.await
|
||||
.map_err(|_e| Error::NotFound)?;
|
||||
Ok(addressbook.into())
|
||||
}
|
||||
|
||||
async fn get_members(
|
||||
&self,
|
||||
(principal, addressbook_id): &Self::PathComponents,
|
||||
) -> Result<Vec<(String, Self::MemberType)>, Self::Error> {
|
||||
Ok(self
|
||||
.addr_store
|
||||
.get_objects(principal, addressbook_id)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|object| {
|
||||
(
|
||||
format!("{}.vcf", object.get_id()),
|
||||
AddressObjectResource {
|
||||
object,
|
||||
principal: principal.to_owned(),
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn save_resource(
|
||||
&self,
|
||||
(principal, addressbook_id): &Self::PathComponents,
|
||||
file: Self::Resource,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.addr_store
|
||||
.update_addressbook(principal.to_owned(), addressbook_id.to_owned(), file.into())
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete_resource(
|
||||
&self,
|
||||
(principal, addressbook_id): &Self::PathComponents,
|
||||
use_trashbin: bool,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.addr_store
|
||||
.delete_addressbook(principal, addressbook_id, use_trashbin)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn actix_additional_routes(res: actix_web::Resource) -> actix_web::Resource {
|
||||
let mkcol_method = web::method(Method::from_str("MKCOL").unwrap());
|
||||
let report_method = web::method(Method::from_str("REPORT").unwrap());
|
||||
res.route(mkcol_method.to(route_mkcol::<AS>))
|
||||
.route(report_method.to(route_report_addressbook::<AS>))
|
||||
.post(route_post::<AS, S>)
|
||||
}
|
||||
}
|
||||
|
||||
146
crates/carddav/src/addressbook/service.rs
Normal file
146
crates/carddav/src/addressbook/service.rs
Normal file
@@ -0,0 +1,146 @@
|
||||
use super::methods::mkcol::route_mkcol;
|
||||
use super::methods::report::route_report_addressbook;
|
||||
use crate::address_object::AddressObjectResourceService;
|
||||
use crate::address_object::resource::AddressObjectResource;
|
||||
use crate::addressbook::methods::get::route_get;
|
||||
use crate::addressbook::methods::put::route_put;
|
||||
use crate::addressbook::resource::AddressbookResource;
|
||||
use crate::{CardDavPrincipalUri, Error};
|
||||
use async_trait::async_trait;
|
||||
use axum::Router;
|
||||
use axum::extract::Request;
|
||||
use axum::handler::Handler;
|
||||
use axum::response::Response;
|
||||
use futures_util::future::BoxFuture;
|
||||
use rustical_dav::resource::{AxumMethods, ResourceService};
|
||||
use rustical_store::auth::User;
|
||||
use rustical_store::{AddressbookStore, SubscriptionStore};
|
||||
use std::convert::Infallible;
|
||||
use std::sync::Arc;
|
||||
use tower::Service;
|
||||
|
||||
pub struct AddressbookResourceService<AS: AddressbookStore, S: SubscriptionStore> {
|
||||
pub(crate) addr_store: Arc<AS>,
|
||||
pub(crate) sub_store: Arc<S>,
|
||||
}
|
||||
|
||||
impl<A: AddressbookStore, S: SubscriptionStore> AddressbookResourceService<A, S> {
|
||||
pub fn new(addr_store: Arc<A>, sub_store: Arc<S>) -> Self {
|
||||
Self {
|
||||
addr_store,
|
||||
sub_store,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: AddressbookStore, S: SubscriptionStore> Clone for AddressbookResourceService<A, S> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
addr_store: self.addr_store.clone(),
|
||||
sub_store: self.sub_store.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<AS: AddressbookStore, S: SubscriptionStore> ResourceService
|
||||
for AddressbookResourceService<AS, S>
|
||||
{
|
||||
type MemberType = AddressObjectResource;
|
||||
type PathComponents = (String, String); // principal, addressbook_id
|
||||
type Resource = AddressbookResource;
|
||||
type Error = Error;
|
||||
type Principal = User;
|
||||
type PrincipalUri = CardDavPrincipalUri;
|
||||
|
||||
const DAV_HEADER: &str = "1, 3, access-control, addressbook";
|
||||
|
||||
async fn get_resource(
|
||||
&self,
|
||||
(principal, addressbook_id): &Self::PathComponents,
|
||||
) -> Result<Self::Resource, Error> {
|
||||
let addressbook = self
|
||||
.addr_store
|
||||
.get_addressbook(principal, addressbook_id, false)
|
||||
.await
|
||||
.map_err(|_e| Error::NotFound)?;
|
||||
Ok(addressbook.into())
|
||||
}
|
||||
|
||||
async fn get_members(
|
||||
&self,
|
||||
(principal, addressbook_id): &Self::PathComponents,
|
||||
) -> Result<Vec<Self::MemberType>, Self::Error> {
|
||||
Ok(self
|
||||
.addr_store
|
||||
.get_objects(principal, addressbook_id)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|object| AddressObjectResource {
|
||||
object,
|
||||
principal: principal.to_owned(),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn save_resource(
|
||||
&self,
|
||||
(principal, addressbook_id): &Self::PathComponents,
|
||||
file: Self::Resource,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.addr_store
|
||||
.update_addressbook(principal.to_owned(), addressbook_id.to_owned(), file.into())
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete_resource(
|
||||
&self,
|
||||
(principal, addressbook_id): &Self::PathComponents,
|
||||
use_trashbin: bool,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.addr_store
|
||||
.delete_addressbook(principal, addressbook_id, use_trashbin)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn axum_router<State: Send + Sync + Clone + 'static>(self) -> Router<State> {
|
||||
Router::new()
|
||||
.nest(
|
||||
"/{object_id}",
|
||||
AddressObjectResourceService::new(self.addr_store.clone()).axum_router(),
|
||||
)
|
||||
.route_service("/", self.axum_service())
|
||||
}
|
||||
}
|
||||
|
||||
impl<AS: AddressbookStore, S: SubscriptionStore> AxumMethods for AddressbookResourceService<AS, S> {
|
||||
fn report() -> Option<fn(Self, Request) -> BoxFuture<'static, Result<Response, Infallible>>> {
|
||||
Some(|state, req| {
|
||||
let mut service = Handler::with_state(route_report_addressbook::<AS, S>, state);
|
||||
Box::pin(Service::call(&mut service, req))
|
||||
})
|
||||
}
|
||||
|
||||
fn get() -> Option<fn(Self, Request) -> BoxFuture<'static, Result<Response, Infallible>>> {
|
||||
Some(|state, req| {
|
||||
let mut service = Handler::with_state(route_get::<AS, S>, state);
|
||||
Box::pin(Service::call(&mut service, req))
|
||||
})
|
||||
}
|
||||
|
||||
fn put() -> Option<fn(Self, Request) -> BoxFuture<'static, Result<Response, Infallible>>> {
|
||||
Some(|state, req| {
|
||||
let mut service = Handler::with_state(route_put::<AS, S>, state);
|
||||
Box::pin(Service::call(&mut service, req))
|
||||
})
|
||||
}
|
||||
|
||||
fn mkcol() -> Option<fn(Self, Request) -> BoxFuture<'static, Result<Response, Infallible>>> {
|
||||
Some(|state, req| {
|
||||
let mut service = Handler::with_state(route_mkcol::<AS, S>, state);
|
||||
Box::pin(Service::call(&mut service, req))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
use actix_web::{HttpResponse, http::StatusCode};
|
||||
use axum::response::IntoResponse;
|
||||
use http::StatusCode;
|
||||
use tracing::error;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
@@ -23,16 +24,17 @@ pub enum Error {
|
||||
|
||||
#[error(transparent)]
|
||||
XmlDecodeError(#[from] rustical_xml::XmlError),
|
||||
|
||||
#[error(transparent)]
|
||||
IcalError(#[from] rustical_ical::Error),
|
||||
}
|
||||
|
||||
impl actix_web::ResponseError for Error {
|
||||
fn status_code(&self) -> actix_web::http::StatusCode {
|
||||
impl Error {
|
||||
pub fn status_code(&self) -> StatusCode {
|
||||
match self {
|
||||
Error::StoreError(err) => match err {
|
||||
rustical_store::Error::NotFound => StatusCode::NOT_FOUND,
|
||||
rustical_store::Error::InvalidData(_) => StatusCode::BAD_REQUEST,
|
||||
rustical_store::Error::AlreadyExists => StatusCode::CONFLICT,
|
||||
rustical_store::Error::ParserError(_) => StatusCode::BAD_REQUEST,
|
||||
rustical_store::Error::ReadOnly => StatusCode::FORBIDDEN,
|
||||
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
},
|
||||
@@ -42,13 +44,13 @@ impl actix_web::ResponseError for Error {
|
||||
Error::XmlDecodeError(_) => StatusCode::BAD_REQUEST,
|
||||
Error::NotImplemented => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Error::NotFound => StatusCode::NOT_FOUND,
|
||||
}
|
||||
}
|
||||
fn error_response(&self) -> actix_web::HttpResponse<actix_web::body::BoxBody> {
|
||||
error!("Error: {self}");
|
||||
match self {
|
||||
Error::DavError(err) => err.error_response(),
|
||||
_ => HttpResponse::build(self.status_code()).body(self.to_string()),
|
||||
Self::IcalError(err) => err.status_code(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponse for Error {
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
(self.status_code(), self.to_string()).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,15 @@
|
||||
use actix_web::{
|
||||
HttpResponse,
|
||||
dev::{HttpServiceFactory, ServiceResponse},
|
||||
http::{
|
||||
Method, StatusCode,
|
||||
header::{self, HeaderName, HeaderValue},
|
||||
},
|
||||
middleware::{ErrorHandlerResponse, ErrorHandlers},
|
||||
web::{self, Data},
|
||||
};
|
||||
use address_object::resource::AddressObjectResourceService;
|
||||
use addressbook::resource::AddressbookResourceService;
|
||||
use axum::response::Redirect;
|
||||
use axum::routing::any;
|
||||
use axum::{Extension, Router};
|
||||
use derive_more::Constructor;
|
||||
pub use error::Error;
|
||||
use principal::{PrincipalResource, PrincipalResourceService};
|
||||
use rustical_dav::resource::{NamedRoute, ResourceService};
|
||||
use principal::PrincipalResourceService;
|
||||
use rustical_dav::resource::{PrincipalUri, ResourceService};
|
||||
use rustical_dav::resources::RootResourceService;
|
||||
use rustical_store::auth::middleware::AuthenticationLayer;
|
||||
use rustical_store::{
|
||||
AddressbookStore, SubscriptionStore,
|
||||
auth::{AuthenticationMiddleware, AuthenticationProvider, User},
|
||||
auth::{AuthenticationProvider, User},
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -25,61 +18,36 @@ pub mod addressbook;
|
||||
pub mod error;
|
||||
pub mod principal;
|
||||
|
||||
pub fn carddav_service<AP: AuthenticationProvider, A: AddressbookStore, S: SubscriptionStore>(
|
||||
#[derive(Debug, Clone, Constructor)]
|
||||
pub struct CardDavPrincipalUri(&'static str);
|
||||
|
||||
impl PrincipalUri for CardDavPrincipalUri {
|
||||
fn principal_uri(&self, principal: &str) -> String {
|
||||
format!("{}/principal/{}/", self.0, principal)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn carddav_router<AP: AuthenticationProvider, A: AddressbookStore, S: SubscriptionStore>(
|
||||
prefix: &'static str,
|
||||
auth_provider: Arc<AP>,
|
||||
store: Arc<A>,
|
||||
subscription_store: Arc<S>,
|
||||
) -> impl HttpServiceFactory {
|
||||
web::scope("")
|
||||
.wrap(AuthenticationMiddleware::new(auth_provider.clone()))
|
||||
.wrap(
|
||||
ErrorHandlers::new().handler(StatusCode::METHOD_NOT_ALLOWED, |res| {
|
||||
Ok(ErrorHandlerResponse::Response(
|
||||
if res.request().method() == Method::OPTIONS {
|
||||
let mut response = HttpResponse::Ok();
|
||||
response.insert_header((
|
||||
HeaderName::from_static("dav"),
|
||||
// https://datatracker.ietf.org/doc/html/rfc4918#section-18
|
||||
HeaderValue::from_static(
|
||||
"1, 3, access-control, addressbook, extended-mkcol, webdav-push",
|
||||
),
|
||||
));
|
||||
|
||||
if let Some(allow) = res.headers().get(header::ALLOW) {
|
||||
response.insert_header((header::ALLOW, allow.to_owned()));
|
||||
}
|
||||
ServiceResponse::new(res.into_parts().0, response.finish())
|
||||
.map_into_right_body()
|
||||
} else {
|
||||
res.map_into_left_body()
|
||||
},
|
||||
))
|
||||
}),
|
||||
) -> Router {
|
||||
let principal_service = PrincipalResourceService::new(
|
||||
store.clone(),
|
||||
auth_provider.clone(),
|
||||
subscription_store.clone(),
|
||||
);
|
||||
Router::new()
|
||||
.nest(
|
||||
prefix,
|
||||
RootResourceService::<_, User, CardDavPrincipalUri>::new(principal_service.clone())
|
||||
.axum_router()
|
||||
.layer(AuthenticationLayer::new(auth_provider))
|
||||
.layer(Extension(CardDavPrincipalUri(prefix))),
|
||||
)
|
||||
.app_data(Data::from(store.clone()))
|
||||
.app_data(Data::from(subscription_store))
|
||||
.service(RootResourceService::<PrincipalResource, User>::default().actix_resource())
|
||||
.service(
|
||||
web::scope("/principal").service(
|
||||
web::scope("/{principal}")
|
||||
.service(
|
||||
PrincipalResourceService::new(store.clone(), auth_provider)
|
||||
.actix_resource()
|
||||
.name(PrincipalResource::route_name()),
|
||||
)
|
||||
.service(
|
||||
web::scope("/{addressbook_id}")
|
||||
.service(
|
||||
AddressbookResourceService::<A, S>::new(store.clone())
|
||||
.actix_resource(),
|
||||
)
|
||||
.service(
|
||||
web::scope("/{object_id}.vcf").service(
|
||||
AddressObjectResourceService::<A>::new(store.clone())
|
||||
.actix_resource(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
.route(
|
||||
"/.well-known/carddav",
|
||||
any(async || Redirect::permanent(prefix)),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,84 +1,33 @@
|
||||
use crate::Error;
|
||||
use crate::addressbook::resource::AddressbookResource;
|
||||
use actix_web::dev::ResourceMap;
|
||||
use async_trait::async_trait;
|
||||
use rustical_dav::extensions::{CommonPropertiesExtension, CommonPropertiesProp};
|
||||
use rustical_dav::extensions::CommonPropertiesExtension;
|
||||
use rustical_dav::privileges::UserPrivilegeSet;
|
||||
use rustical_dav::resource::{NamedRoute, Resource, ResourceService};
|
||||
use rustical_dav::resource::{PrincipalUri, Resource, ResourceName};
|
||||
use rustical_dav::xml::{HrefElement, Resourcetype, ResourcetypeInner};
|
||||
use rustical_store::AddressbookStore;
|
||||
use rustical_store::auth::{AuthenticationProvider, User};
|
||||
use rustical_xml::{EnumUnitVariants, EnumVariants, XmlDeserialize, XmlSerialize};
|
||||
use std::sync::Arc;
|
||||
use rustical_store::auth::User;
|
||||
|
||||
pub struct PrincipalResourceService<A: AddressbookStore, AP: AuthenticationProvider> {
|
||||
addr_store: Arc<A>,
|
||||
auth_provider: Arc<AP>,
|
||||
}
|
||||
|
||||
impl<A: AddressbookStore, AP: AuthenticationProvider> PrincipalResourceService<A, AP> {
|
||||
pub fn new(addr_store: Arc<A>, auth_provider: Arc<AP>) -> Self {
|
||||
Self {
|
||||
addr_store,
|
||||
auth_provider,
|
||||
}
|
||||
}
|
||||
}
|
||||
mod service;
|
||||
pub use service::*;
|
||||
mod prop;
|
||||
pub use prop::*;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PrincipalResource {
|
||||
principal: User,
|
||||
}
|
||||
|
||||
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone)]
|
||||
pub struct AddressbookHomeSet(#[xml(ty = "untagged", flatten)] Vec<HrefElement>);
|
||||
|
||||
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, EnumUnitVariants)]
|
||||
#[xml(unit_variants_ident = "PrincipalPropName")]
|
||||
pub enum PrincipalProp {
|
||||
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
|
||||
Displayname(String),
|
||||
|
||||
// WebDAV Access Control (RFC 3744)
|
||||
#[xml(rename = b"principal-URL")]
|
||||
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
|
||||
PrincipalUrl(HrefElement),
|
||||
|
||||
// CardDAV (RFC 6352)
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV")]
|
||||
AddressbookHomeSet(AddressbookHomeSet),
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV")]
|
||||
PrincipalAddress(Option<HrefElement>),
|
||||
}
|
||||
|
||||
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, EnumUnitVariants)]
|
||||
#[xml(unit_variants_ident = "PrincipalPropWrapperName", untagged)]
|
||||
pub enum PrincipalPropWrapper {
|
||||
Principal(PrincipalProp),
|
||||
Common(CommonPropertiesProp),
|
||||
}
|
||||
|
||||
impl PrincipalResource {
|
||||
pub fn get_principal_url(rmap: &ResourceMap, principal: &str) -> String {
|
||||
Self::get_url(rmap, vec![principal]).unwrap()
|
||||
impl ResourceName for PrincipalResource {
|
||||
fn get_name(&self) -> String {
|
||||
self.principal.id.to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
impl NamedRoute for PrincipalResource {
|
||||
fn route_name() -> &'static str {
|
||||
"carddav_principal"
|
||||
}
|
||||
}
|
||||
|
||||
impl CommonPropertiesExtension for PrincipalResource {
|
||||
type PrincipalResource = Self;
|
||||
}
|
||||
|
||||
impl Resource for PrincipalResource {
|
||||
type Prop = PrincipalPropWrapper;
|
||||
type Error = Error;
|
||||
type Principal = User;
|
||||
|
||||
const IS_COLLECTION: bool = true;
|
||||
|
||||
fn get_resourcetype(&self) -> Resourcetype {
|
||||
Resourcetype(&[
|
||||
ResourcetypeInner(Some(rustical_dav::namespace::NS_DAV), "collection"),
|
||||
@@ -88,16 +37,16 @@ impl Resource for PrincipalResource {
|
||||
|
||||
fn get_prop(
|
||||
&self,
|
||||
rmap: &ResourceMap,
|
||||
puri: &impl PrincipalUri,
|
||||
user: &User,
|
||||
prop: &PrincipalPropWrapperName,
|
||||
) -> Result<Self::Prop, Self::Error> {
|
||||
let principal_href = HrefElement::new(Self::get_principal_url(rmap, &self.principal.id));
|
||||
let principal_href = HrefElement::new(puri.principal_uri(&user.id));
|
||||
|
||||
let home_set = AddressbookHomeSet(
|
||||
user.memberships()
|
||||
.into_iter()
|
||||
.map(|principal| Self::get_url(rmap, vec![principal]).unwrap())
|
||||
.map(|principal| puri.principal_uri(principal))
|
||||
.map(HrefElement::new)
|
||||
.collect(),
|
||||
);
|
||||
@@ -120,7 +69,7 @@ impl Resource for PrincipalResource {
|
||||
}
|
||||
|
||||
PrincipalPropWrapperName::Common(prop) => PrincipalPropWrapper::Common(
|
||||
CommonPropertiesExtension::get_prop(self, rmap, user, prop)?,
|
||||
CommonPropertiesExtension::get_prop(self, puri, user, prop)?,
|
||||
),
|
||||
})
|
||||
}
|
||||
@@ -135,37 +84,3 @@ impl Resource for PrincipalResource {
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl<A: AddressbookStore, AP: AuthenticationProvider> ResourceService
|
||||
for PrincipalResourceService<A, AP>
|
||||
{
|
||||
type PathComponents = (String,);
|
||||
type MemberType = AddressbookResource;
|
||||
type Resource = PrincipalResource;
|
||||
type Error = Error;
|
||||
type Principal = User;
|
||||
|
||||
async fn get_resource(
|
||||
&self,
|
||||
(principal,): &Self::PathComponents,
|
||||
) -> Result<Self::Resource, Self::Error> {
|
||||
let user = self
|
||||
.auth_provider
|
||||
.get_principal(principal)
|
||||
.await?
|
||||
.ok_or(crate::Error::NotFound)?;
|
||||
Ok(PrincipalResource { principal: user })
|
||||
}
|
||||
|
||||
async fn get_members(
|
||||
&self,
|
||||
(principal,): &Self::PathComponents,
|
||||
) -> Result<Vec<(String, Self::MemberType)>, Self::Error> {
|
||||
let addressbooks = self.addr_store.get_addressbooks(principal).await?;
|
||||
Ok(addressbooks
|
||||
.into_iter()
|
||||
.map(|addressbook| (addressbook.id.to_owned(), addressbook.into()))
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
30
crates/carddav/src/principal/prop.rs
Normal file
30
crates/carddav/src/principal/prop.rs
Normal file
@@ -0,0 +1,30 @@
|
||||
use rustical_dav::{extensions::CommonPropertiesProp, xml::HrefElement};
|
||||
use rustical_xml::{EnumVariants, PropName, XmlDeserialize, XmlSerialize};
|
||||
|
||||
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone)]
|
||||
pub struct AddressbookHomeSet(#[xml(ty = "untagged", flatten)] pub(super) Vec<HrefElement>);
|
||||
|
||||
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, PropName)]
|
||||
#[xml(unit_variants_ident = "PrincipalPropName")]
|
||||
pub enum PrincipalProp {
|
||||
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
|
||||
Displayname(String),
|
||||
|
||||
// WebDAV Access Control (RFC 3744)
|
||||
#[xml(rename = b"principal-URL")]
|
||||
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
|
||||
PrincipalUrl(HrefElement),
|
||||
|
||||
// CardDAV (RFC 6352)
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV")]
|
||||
AddressbookHomeSet(AddressbookHomeSet),
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV")]
|
||||
PrincipalAddress(Option<HrefElement>),
|
||||
}
|
||||
|
||||
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, PropName)]
|
||||
#[xml(unit_variants_ident = "PrincipalPropWrapperName", untagged)]
|
||||
pub enum PrincipalPropWrapper {
|
||||
Principal(PrincipalProp),
|
||||
Common(CommonPropertiesProp),
|
||||
}
|
||||
96
crates/carddav/src/principal/service.rs
Normal file
96
crates/carddav/src/principal/service.rs
Normal file
@@ -0,0 +1,96 @@
|
||||
use crate::addressbook::AddressbookResourceService;
|
||||
use crate::addressbook::resource::AddressbookResource;
|
||||
use crate::principal::PrincipalResource;
|
||||
use crate::{CardDavPrincipalUri, Error};
|
||||
use async_trait::async_trait;
|
||||
use axum::Router;
|
||||
use rustical_dav::resource::{AxumMethods, ResourceService};
|
||||
use rustical_store::auth::{AuthenticationProvider, User};
|
||||
use rustical_store::{AddressbookStore, SubscriptionStore};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct PrincipalResourceService<
|
||||
A: AddressbookStore,
|
||||
AP: AuthenticationProvider,
|
||||
S: SubscriptionStore,
|
||||
> {
|
||||
addr_store: Arc<A>,
|
||||
auth_provider: Arc<AP>,
|
||||
sub_store: Arc<S>,
|
||||
}
|
||||
|
||||
impl<A: AddressbookStore, AP: AuthenticationProvider, S: SubscriptionStore> Clone
|
||||
for PrincipalResourceService<A, AP, S>
|
||||
{
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
addr_store: self.addr_store.clone(),
|
||||
auth_provider: self.auth_provider.clone(),
|
||||
sub_store: self.sub_store.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: AddressbookStore, AP: AuthenticationProvider, S: SubscriptionStore>
|
||||
PrincipalResourceService<A, AP, S>
|
||||
{
|
||||
pub fn new(addr_store: Arc<A>, auth_provider: Arc<AP>, sub_store: Arc<S>) -> Self {
|
||||
Self {
|
||||
addr_store,
|
||||
auth_provider,
|
||||
sub_store,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<A: AddressbookStore, AP: AuthenticationProvider, S: SubscriptionStore> ResourceService
|
||||
for PrincipalResourceService<A, AP, S>
|
||||
{
|
||||
type PathComponents = (String,);
|
||||
type MemberType = AddressbookResource;
|
||||
type Resource = PrincipalResource;
|
||||
type Error = Error;
|
||||
type Principal = User;
|
||||
type PrincipalUri = CardDavPrincipalUri;
|
||||
|
||||
const DAV_HEADER: &str = "1, 3, access-control, addressbook";
|
||||
|
||||
async fn get_resource(
|
||||
&self,
|
||||
(principal,): &Self::PathComponents,
|
||||
) -> Result<Self::Resource, Self::Error> {
|
||||
let user = self
|
||||
.auth_provider
|
||||
.get_principal(principal)
|
||||
.await?
|
||||
.ok_or(crate::Error::NotFound)?;
|
||||
Ok(PrincipalResource { principal: user })
|
||||
}
|
||||
|
||||
async fn get_members(
|
||||
&self,
|
||||
(principal,): &Self::PathComponents,
|
||||
) -> Result<Vec<Self::MemberType>, Self::Error> {
|
||||
let addressbooks = self.addr_store.get_addressbooks(principal).await?;
|
||||
Ok(addressbooks
|
||||
.into_iter()
|
||||
.map(AddressbookResource::from)
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn axum_router<State: Send + Sync + Clone + 'static>(self) -> Router<State> {
|
||||
Router::new()
|
||||
.nest(
|
||||
"/{addressbook_id}",
|
||||
AddressbookResourceService::new(self.addr_store.clone(), self.sub_store.clone())
|
||||
.axum_router(),
|
||||
)
|
||||
.route_service("/", self.axum_service())
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: AddressbookStore, AP: AuthenticationProvider, S: SubscriptionStore> AxumMethods
|
||||
for PrincipalResourceService<A, AP, S>
|
||||
{
|
||||
}
|
||||
@@ -7,8 +7,11 @@ repository.workspace = true
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
axum.workspace = true
|
||||
tower.workspace = true
|
||||
axum-extra.workspace = true
|
||||
|
||||
rustical_xml.workspace = true
|
||||
actix-web.workspace = true
|
||||
async-trait.workspace = true
|
||||
futures-util.workspace = true
|
||||
quick-xml.workspace = true
|
||||
@@ -18,5 +21,6 @@ itertools.workspace = true
|
||||
log.workspace = true
|
||||
derive_more.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-actix-web.workspace = true
|
||||
tokio.workspace = true
|
||||
http.workspace = true
|
||||
headers.workspace = true
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use actix_web::{http::StatusCode, HttpResponse};
|
||||
use http::StatusCode;
|
||||
use rustical_xml::XmlError;
|
||||
use thiserror::Error;
|
||||
use tracing::error;
|
||||
@@ -25,10 +25,13 @@ pub enum Error {
|
||||
|
||||
#[error(transparent)]
|
||||
IOError(#[from] std::io::Error),
|
||||
|
||||
#[error("Precondition Failed")]
|
||||
PreconditionFailed,
|
||||
}
|
||||
|
||||
impl actix_web::error::ResponseError for Error {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
impl Error {
|
||||
pub fn status_code(&self) -> StatusCode {
|
||||
match self {
|
||||
Self::InternalError => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Self::NotFound => StatusCode::NOT_FOUND,
|
||||
@@ -44,17 +47,24 @@ impl actix_web::error::ResponseError for Error {
|
||||
_ => StatusCode::BAD_REQUEST,
|
||||
},
|
||||
Error::PropReadOnly => StatusCode::CONFLICT,
|
||||
Error::PreconditionFailed => StatusCode::PRECONDITION_FAILED,
|
||||
Self::IOError(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
error!("Error: {self}");
|
||||
match self {
|
||||
Error::Unauthorized => HttpResponse::build(self.status_code())
|
||||
.append_header(("WWW-Authenticate", "Basic"))
|
||||
.body(self.to_string()),
|
||||
_ => HttpResponse::build(self.status_code()).body(self.to_string()),
|
||||
impl axum::response::IntoResponse for Error {
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
use axum::body::Body;
|
||||
|
||||
let mut resp = axum::response::Response::builder().status(self.status_code());
|
||||
if matches!(&self, &Error::Unauthorized) {
|
||||
resp.headers_mut()
|
||||
.expect("This must always work")
|
||||
.insert("WWW-Authenticate", "Basic".parse().unwrap());
|
||||
}
|
||||
|
||||
resp.body(Body::new(self.to_string()))
|
||||
.expect("This should always work")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
use crate::{
|
||||
Principal,
|
||||
privileges::UserPrivilegeSet,
|
||||
resource::{NamedRoute, Resource},
|
||||
resource::{PrincipalUri, Resource},
|
||||
xml::{HrefElement, Resourcetype},
|
||||
};
|
||||
use actix_web::dev::ResourceMap;
|
||||
use rustical_xml::{EnumUnitVariants, EnumVariants, XmlDeserialize, XmlSerialize};
|
||||
use rustical_xml::{EnumVariants, PropName, XmlDeserialize, XmlSerialize};
|
||||
|
||||
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumUnitVariants, EnumVariants)]
|
||||
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, PropName, EnumVariants)]
|
||||
#[xml(unit_variants_ident = "CommonPropertiesPropName")]
|
||||
pub enum CommonPropertiesProp {
|
||||
// WebDAV (RFC 2518)
|
||||
@@ -28,11 +27,9 @@ pub enum CommonPropertiesProp {
|
||||
}
|
||||
|
||||
pub trait CommonPropertiesExtension: Resource {
|
||||
type PrincipalResource: NamedRoute;
|
||||
|
||||
fn get_prop(
|
||||
&self,
|
||||
rmap: &ResourceMap,
|
||||
principal_uri: &impl PrincipalUri,
|
||||
principal: &Self::Principal,
|
||||
prop: &CommonPropertiesPropName,
|
||||
) -> Result<CommonPropertiesProp, <Self as Resource>::Error> {
|
||||
@@ -42,21 +39,16 @@ pub trait CommonPropertiesExtension: Resource {
|
||||
}
|
||||
CommonPropertiesPropName::CurrentUserPrincipal => {
|
||||
CommonPropertiesProp::CurrentUserPrincipal(
|
||||
Self::PrincipalResource::get_url(rmap, [&principal.get_id()])
|
||||
.unwrap()
|
||||
.into(),
|
||||
principal_uri.principal_uri(principal.get_id()).into(),
|
||||
)
|
||||
}
|
||||
CommonPropertiesPropName::CurrentUserPrivilegeSet => {
|
||||
CommonPropertiesProp::CurrentUserPrivilegeSet(self.get_user_privileges(principal)?)
|
||||
}
|
||||
CommonPropertiesPropName::Owner => {
|
||||
CommonPropertiesProp::Owner(self.get_owner().map(|owner| {
|
||||
Self::PrincipalResource::get_url(rmap, [owner])
|
||||
.unwrap()
|
||||
.into()
|
||||
}))
|
||||
}
|
||||
CommonPropertiesPropName::Owner => CommonPropertiesProp::Owner(
|
||||
self.get_owner()
|
||||
.map(|owner| principal_uri.principal_uri(owner).into()),
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -68,3 +60,5 @@ pub trait CommonPropertiesExtension: Resource {
|
||||
Err(crate::Error::PropReadOnly)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Resource> CommonPropertiesExtension for R {}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use rustical_xml::{EnumUnitVariants, EnumVariants, XmlDeserialize, XmlSerialize};
|
||||
use rustical_xml::{EnumVariants, PropName, XmlDeserialize, XmlSerialize};
|
||||
|
||||
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumUnitVariants, EnumVariants)]
|
||||
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, PropName, EnumVariants)]
|
||||
#[xml(unit_variants_ident = "SyncTokenExtensionPropName")]
|
||||
pub enum SyncTokenExtensionProp {
|
||||
// Collection Synchronization (RFC 6578)
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use actix_web::{FromRequest, HttpRequest, ResponseError, http::StatusCode};
|
||||
use futures_util::future::{Ready, err, ok};
|
||||
use axum::{
|
||||
body::Body,
|
||||
extract::{FromRequestParts, OptionalFromRequestParts},
|
||||
response::IntoResponse,
|
||||
};
|
||||
use rustical_xml::{ValueDeserialize, ValueSerialize, XmlError};
|
||||
use thiserror::Error;
|
||||
|
||||
@@ -7,9 +10,12 @@ use thiserror::Error;
|
||||
#[error("Invalid Depth header")]
|
||||
pub struct InvalidDepthHeader;
|
||||
|
||||
impl ResponseError for InvalidDepthHeader {
|
||||
fn status_code(&self) -> actix_web::http::StatusCode {
|
||||
StatusCode::BAD_REQUEST
|
||||
impl IntoResponse for InvalidDepthHeader {
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
axum::response::Response::builder()
|
||||
.status(axum::http::StatusCode::BAD_REQUEST)
|
||||
.body(Body::empty())
|
||||
.expect("this always works")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,23 +63,32 @@ impl TryFrom<&[u8]> for Depth {
|
||||
}
|
||||
}
|
||||
|
||||
impl FromRequest for Depth {
|
||||
type Error = InvalidDepthHeader;
|
||||
type Future = Ready<Result<Self, Self::Error>>;
|
||||
impl<S: Send + Sync> OptionalFromRequestParts<S> for Depth {
|
||||
type Rejection = InvalidDepthHeader;
|
||||
|
||||
fn extract(req: &HttpRequest) -> Self::Future {
|
||||
if let Some(depth_header) = req.headers().get("Depth") {
|
||||
match depth_header.as_bytes().try_into() {
|
||||
Ok(depth) => ok(depth),
|
||||
Err(e) => err(e),
|
||||
}
|
||||
async fn from_request_parts(
|
||||
parts: &mut axum::http::request::Parts,
|
||||
_state: &S,
|
||||
) -> Result<Option<Self>, Self::Rejection> {
|
||||
if let Some(depth_header) = parts.headers.get("Depth") {
|
||||
Ok(Some(depth_header.as_bytes().try_into()?))
|
||||
} else {
|
||||
// default depth
|
||||
ok(Depth::Zero)
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn from_request(req: &HttpRequest, _payload: &mut actix_web::dev::Payload) -> Self::Future {
|
||||
Self::extract(req)
|
||||
impl<S: Send + Sync> FromRequestParts<S> for Depth {
|
||||
type Rejection = InvalidDepthHeader;
|
||||
|
||||
async fn from_request_parts(
|
||||
parts: &mut axum::http::request::Parts,
|
||||
_state: &S,
|
||||
) -> Result<Self, Self::Rejection> {
|
||||
if let Some(depth_header) = parts.headers.get("Depth") {
|
||||
depth_header.as_bytes().try_into()
|
||||
} else {
|
||||
Ok(Self::Zero)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
use actix_web::{FromRequest, HttpRequest, ResponseError, http::StatusCode};
|
||||
use futures_util::future::{Ready, err, ok};
|
||||
use axum::{body::Body, extract::FromRequestParts, response::IntoResponse};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
#[error("Invalid Overwrite header")]
|
||||
pub struct InvalidOverwriteHeader;
|
||||
|
||||
impl ResponseError for InvalidOverwriteHeader {
|
||||
fn status_code(&self) -> actix_web::http::StatusCode {
|
||||
StatusCode::BAD_REQUEST
|
||||
impl IntoResponse for InvalidOverwriteHeader {
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
axum::response::Response::builder()
|
||||
.status(axum::http::StatusCode::BAD_REQUEST)
|
||||
.body(Body::new("Invalid Overwrite header".to_string()))
|
||||
.expect("this always works")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +27,21 @@ impl Overwrite {
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: Send + Sync> FromRequestParts<S> for Overwrite {
|
||||
type Rejection = InvalidOverwriteHeader;
|
||||
|
||||
async fn from_request_parts(
|
||||
parts: &mut axum::http::request::Parts,
|
||||
_state: &S,
|
||||
) -> Result<Self, Self::Rejection> {
|
||||
if let Some(overwrite_header) = parts.headers.get("Overwrite") {
|
||||
overwrite_header.as_bytes().try_into()
|
||||
} else {
|
||||
Ok(Self::default())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&[u8]> for Overwrite {
|
||||
type Error = InvalidOverwriteHeader;
|
||||
|
||||
@@ -36,24 +53,3 @@ impl TryFrom<&[u8]> for Overwrite {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromRequest for Overwrite {
|
||||
type Error = InvalidOverwriteHeader;
|
||||
type Future = Ready<Result<Self, Self::Error>>;
|
||||
|
||||
fn extract(req: &HttpRequest) -> Self::Future {
|
||||
if let Some(overwrite_header) = req.headers().get("Overwrite") {
|
||||
match overwrite_header.as_bytes().try_into() {
|
||||
Ok(depth) => ok(depth),
|
||||
Err(e) => err(e),
|
||||
}
|
||||
} else {
|
||||
// default depth
|
||||
ok(Overwrite::F)
|
||||
}
|
||||
}
|
||||
|
||||
fn from_request(req: &HttpRequest, _payload: &mut actix_web::dev::Payload) -> Self::Future {
|
||||
Self::extract(req)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,10 +6,8 @@ pub mod privileges;
|
||||
pub mod resource;
|
||||
pub mod resources;
|
||||
pub mod xml;
|
||||
|
||||
use actix_web::FromRequest;
|
||||
pub use error::Error;
|
||||
|
||||
pub trait Principal: std::fmt::Debug + Clone + FromRequest + 'static {
|
||||
pub trait Principal: std::fmt::Debug + Clone + Send + Sync + 'static {
|
||||
fn get_id(&self) -> &str;
|
||||
}
|
||||
|
||||
80
crates/dav/src/resource/axum_methods.rs
Normal file
80
crates/dav/src/resource/axum_methods.rs
Normal file
@@ -0,0 +1,80 @@
|
||||
use axum::{extract::Request, response::Response};
|
||||
use futures_util::future::BoxFuture;
|
||||
use headers::Allow;
|
||||
use http::Method;
|
||||
use std::{convert::Infallible, str::FromStr};
|
||||
|
||||
pub type MethodFunction<State> =
|
||||
fn(State, Request) -> BoxFuture<'static, Result<Response, Infallible>>;
|
||||
|
||||
pub trait AxumMethods: Sized + Send + Sync + 'static {
|
||||
#[inline]
|
||||
fn report() -> Option<MethodFunction<Self>> {
|
||||
None
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn get() -> Option<MethodFunction<Self>> {
|
||||
None
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn head() -> Option<MethodFunction<Self>> {
|
||||
None
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn post() -> Option<MethodFunction<Self>> {
|
||||
None
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn mkcol() -> Option<MethodFunction<Self>> {
|
||||
None
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn mkcalendar() -> Option<MethodFunction<Self>> {
|
||||
None
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn put() -> Option<MethodFunction<Self>> {
|
||||
None
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn allow_header() -> Allow {
|
||||
let mut allow = vec![
|
||||
Method::from_str("PROPFIND").unwrap(),
|
||||
Method::from_str("PROPPATCH").unwrap(),
|
||||
Method::from_str("COPY").unwrap(),
|
||||
Method::from_str("MOVE").unwrap(),
|
||||
Method::DELETE,
|
||||
Method::OPTIONS,
|
||||
];
|
||||
if Self::report().is_some() {
|
||||
allow.push(Method::from_str("REPORT").unwrap());
|
||||
}
|
||||
if Self::get().is_some() {
|
||||
allow.push(Method::GET);
|
||||
}
|
||||
if Self::head().is_some() {
|
||||
allow.push(Method::HEAD);
|
||||
}
|
||||
if Self::post().is_some() {
|
||||
allow.push(Method::POST);
|
||||
}
|
||||
if Self::mkcol().is_some() {
|
||||
allow.push(Method::from_str("MKCOL").unwrap());
|
||||
}
|
||||
if Self::mkcalendar().is_some() {
|
||||
allow.push(Method::from_str("MKCALENDAR").unwrap());
|
||||
}
|
||||
if Self::put().is_some() {
|
||||
allow.push(Method::PUT);
|
||||
}
|
||||
|
||||
allow.into_iter().collect()
|
||||
}
|
||||
}
|
||||
122
crates/dav/src/resource/axum_service.rs
Normal file
122
crates/dav/src/resource/axum_service.rs
Normal file
@@ -0,0 +1,122 @@
|
||||
use super::methods::{axum_route_propfind, axum_route_proppatch};
|
||||
use crate::resource::{
|
||||
ResourceService,
|
||||
axum_methods::AxumMethods,
|
||||
methods::{axum_route_copy, axum_route_move},
|
||||
};
|
||||
use axum::{
|
||||
body::Body,
|
||||
extract::FromRequestParts,
|
||||
handler::Handler,
|
||||
http::{Request, Response},
|
||||
response::IntoResponse,
|
||||
};
|
||||
use futures_util::future::BoxFuture;
|
||||
use headers::HeaderMapExt;
|
||||
use http::{HeaderValue, StatusCode};
|
||||
use std::convert::Infallible;
|
||||
use tower::Service;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AxumService<RS: ResourceService + AxumMethods> {
|
||||
resource_service: RS,
|
||||
}
|
||||
|
||||
impl<RS: ResourceService + AxumMethods> AxumService<RS> {
|
||||
pub fn new(resource_service: RS) -> Self {
|
||||
Self { resource_service }
|
||||
}
|
||||
}
|
||||
|
||||
impl<RS: ResourceService + AxumMethods + Clone + Send + Sync> Service<Request<Body>>
|
||||
for AxumService<RS>
|
||||
where
|
||||
RS::Error: IntoResponse + Send + Sync + 'static,
|
||||
RS::Principal: FromRequestParts<RS>,
|
||||
{
|
||||
type Error = Infallible;
|
||||
type Response = Response<Body>;
|
||||
type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;
|
||||
|
||||
#[inline]
|
||||
fn poll_ready(
|
||||
&mut self,
|
||||
_cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<Result<(), Self::Error>> {
|
||||
Ok(()).into()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn call(&mut self, req: Request<Body>) -> Self::Future {
|
||||
use crate::resource::methods::axum_route_delete;
|
||||
let mut propfind_service =
|
||||
Handler::with_state(axum_route_propfind::<RS>, self.resource_service.clone());
|
||||
let mut proppatch_service =
|
||||
Handler::with_state(axum_route_proppatch::<RS>, self.resource_service.clone());
|
||||
let mut delete_service =
|
||||
Handler::with_state(axum_route_delete::<RS>, self.resource_service.clone());
|
||||
let mut move_service =
|
||||
Handler::with_state(axum_route_move::<RS>, self.resource_service.clone());
|
||||
let mut copy_service =
|
||||
Handler::with_state(axum_route_copy::<RS>, self.resource_service.clone());
|
||||
let mut options_service = Handler::with_state(route_options::<RS>, ());
|
||||
match req.method().as_str() {
|
||||
"PROPFIND" => return Box::pin(Service::call(&mut propfind_service, req)),
|
||||
"PROPPATCH" => return Box::pin(Service::call(&mut proppatch_service, req)),
|
||||
"DELETE" => return Box::pin(Service::call(&mut delete_service, req)),
|
||||
"OPTIONS" => return Box::pin(Service::call(&mut options_service, req)),
|
||||
"MOVE" => return Box::pin(Service::call(&mut move_service, req)),
|
||||
"COPY" => return Box::pin(Service::call(&mut copy_service, req)),
|
||||
"REPORT" => {
|
||||
if let Some(svc) = RS::report() {
|
||||
return svc(self.resource_service.clone(), req);
|
||||
}
|
||||
}
|
||||
"GET" => {
|
||||
if let Some(svc) = RS::get() {
|
||||
return svc(self.resource_service.clone(), req);
|
||||
}
|
||||
}
|
||||
"HEAD" => {
|
||||
if let Some(svc) = RS::head() {
|
||||
return svc(self.resource_service.clone(), req);
|
||||
}
|
||||
}
|
||||
"POST" => {
|
||||
if let Some(svc) = RS::post() {
|
||||
return svc(self.resource_service.clone(), req);
|
||||
}
|
||||
}
|
||||
"MKCOL" => {
|
||||
if let Some(svc) = RS::mkcol() {
|
||||
return svc(self.resource_service.clone(), req);
|
||||
}
|
||||
}
|
||||
"MKCALENDAR" => {
|
||||
if let Some(svc) = RS::mkcalendar() {
|
||||
return svc(self.resource_service.clone(), req);
|
||||
}
|
||||
}
|
||||
"PUT" => {
|
||||
if let Some(svc) = RS::put() {
|
||||
return svc(self.resource_service.clone(), req);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
Box::pin(async move {
|
||||
Ok(Response::builder()
|
||||
.status(StatusCode::METHOD_NOT_ALLOWED)
|
||||
.body(Body::from("Method not allowed"))
|
||||
.unwrap())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn route_options<RS: ResourceService + AxumMethods>() -> Response<Body> {
|
||||
let mut resp = Response::builder().status(StatusCode::OK);
|
||||
let headers = resp.headers_mut().unwrap();
|
||||
headers.insert("DAV", HeaderValue::from_static(RS::DAV_HEADER));
|
||||
headers.typed_insert(RS::allow_header());
|
||||
resp.body(Body::empty()).unwrap()
|
||||
}
|
||||
25
crates/dav/src/resource/methods/copy.rs
Normal file
25
crates/dav/src/resource/methods/copy.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use http::StatusCode;
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::{
|
||||
header::{Depth, Overwrite},
|
||||
resource::ResourceService,
|
||||
};
|
||||
|
||||
#[instrument(skip(_path, _resource_service,))]
|
||||
pub(crate) async fn axum_route_copy<R: ResourceService>(
|
||||
Path(_path): Path<R::PathComponents>,
|
||||
State(_resource_service): State<R>,
|
||||
depth: Option<Depth>,
|
||||
principal: R::Principal,
|
||||
overwrite: Overwrite,
|
||||
) -> Result<Response, R::Error> {
|
||||
// TODO: Actually implement, but to be WebDAV-compliant we must at least support this route but
|
||||
// can return a 403 error
|
||||
let _depth = depth.unwrap_or(Depth::Infinity);
|
||||
Ok(StatusCode::FORBIDDEN.into_response())
|
||||
}
|
||||
@@ -2,50 +2,71 @@ use crate::Error;
|
||||
use crate::privileges::UserPrivilege;
|
||||
use crate::resource::Resource;
|
||||
use crate::resource::ResourceService;
|
||||
use actix_web::HttpRequest;
|
||||
use actix_web::HttpResponse;
|
||||
use actix_web::Responder;
|
||||
use actix_web::http::header::IfMatch;
|
||||
use actix_web::http::header::IfNoneMatch;
|
||||
use actix_web::web;
|
||||
use actix_web::web::Data;
|
||||
use actix_web::web::Path;
|
||||
use tracing::instrument;
|
||||
use tracing_actix_web::RootSpan;
|
||||
use axum::extract::{Path, State};
|
||||
use axum_extra::TypedHeader;
|
||||
use headers::{IfMatch, IfNoneMatch};
|
||||
use http::HeaderMap;
|
||||
|
||||
#[instrument(parent = root_span.id(), skip(path, req, root_span, resource_service))]
|
||||
pub async fn route_delete<R: ResourceService>(
|
||||
path: Path<R::PathComponents>,
|
||||
req: HttpRequest,
|
||||
pub(crate) async fn axum_route_delete<R: ResourceService>(
|
||||
Path(path): Path<R::PathComponents>,
|
||||
State(resource_service): State<R>,
|
||||
principal: R::Principal,
|
||||
resource_service: Data<R>,
|
||||
root_span: RootSpan,
|
||||
if_match: web::Header<IfMatch>,
|
||||
if_none_match: web::Header<IfNoneMatch>,
|
||||
) -> Result<impl Responder, R::Error> {
|
||||
let no_trash = req
|
||||
.headers()
|
||||
mut if_match: Option<TypedHeader<IfMatch>>,
|
||||
mut if_none_match: Option<TypedHeader<IfNoneMatch>>,
|
||||
header_map: HeaderMap,
|
||||
) -> Result<(), R::Error> {
|
||||
// https://github.com/hyperium/headers/issues/204
|
||||
if !header_map.contains_key("If-Match") {
|
||||
if_match = None;
|
||||
}
|
||||
if !header_map.contains_key("If-None-Match") {
|
||||
if_none_match = None;
|
||||
}
|
||||
let no_trash = header_map
|
||||
.get("X-No-Trashbin")
|
||||
.map(|val| matches!(val.to_str(), Ok("1")))
|
||||
.unwrap_or(false);
|
||||
route_delete(
|
||||
&path,
|
||||
&principal,
|
||||
&resource_service,
|
||||
no_trash,
|
||||
if_match.map(|hdr| hdr.0),
|
||||
if_none_match.map(|hdr| hdr.0),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
let resource = resource_service.get_resource(&path).await?;
|
||||
pub async fn route_delete<R: ResourceService>(
|
||||
path_components: &R::PathComponents,
|
||||
principal: &R::Principal,
|
||||
resource_service: &R,
|
||||
no_trash: bool,
|
||||
if_match: Option<IfMatch>,
|
||||
if_none_match: Option<IfNoneMatch>,
|
||||
) -> Result<(), R::Error> {
|
||||
let resource = resource_service.get_resource(path_components).await?;
|
||||
|
||||
let privileges = resource.get_user_privileges(&principal)?;
|
||||
let privileges = resource.get_user_privileges(principal)?;
|
||||
if !privileges.has(&UserPrivilege::Write) {
|
||||
return Err(Error::Unauthorized.into());
|
||||
}
|
||||
|
||||
if !resource.satisfies_if_match(&if_match) {
|
||||
// Precondition failed
|
||||
return Ok(HttpResponse::PreconditionFailed().finish());
|
||||
if let Some(if_match) = if_match {
|
||||
dbg!(&if_match);
|
||||
if !resource.satisfies_if_match(&if_match) {
|
||||
// Precondition failed
|
||||
return Err(crate::Error::PreconditionFailed.into());
|
||||
}
|
||||
}
|
||||
if resource.satisfies_if_none_match(&if_none_match) {
|
||||
// Precondition failed
|
||||
return Ok(HttpResponse::PreconditionFailed().finish());
|
||||
if let Some(if_none_match) = if_none_match {
|
||||
if resource.satisfies_if_none_match(&if_none_match) {
|
||||
// Precondition failed
|
||||
return Err(crate::Error::PreconditionFailed.into());
|
||||
}
|
||||
}
|
||||
|
||||
resource_service.delete_resource(&path, !no_trash).await?;
|
||||
|
||||
Ok(HttpResponse::Ok().body(""))
|
||||
resource_service
|
||||
.delete_resource(path_components, !no_trash)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
mod copy;
|
||||
mod delete;
|
||||
mod mv;
|
||||
mod propfind;
|
||||
mod proppatch;
|
||||
|
||||
pub(crate) use delete::route_delete;
|
||||
pub(crate) use propfind::route_propfind;
|
||||
pub(crate) use proppatch::route_proppatch;
|
||||
pub(crate) use copy::axum_route_copy;
|
||||
pub(crate) use delete::axum_route_delete;
|
||||
pub(crate) use mv::axum_route_move;
|
||||
pub(crate) use propfind::axum_route_propfind;
|
||||
pub(crate) use proppatch::axum_route_proppatch;
|
||||
|
||||
25
crates/dav/src/resource/methods/mv.rs
Normal file
25
crates/dav/src/resource/methods/mv.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use http::StatusCode;
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::{
|
||||
header::{Depth, Overwrite},
|
||||
resource::ResourceService,
|
||||
};
|
||||
|
||||
#[instrument(skip(_path, _resource_service,))]
|
||||
pub(crate) async fn axum_route_move<R: ResourceService>(
|
||||
Path(_path): Path<R::PathComponents>,
|
||||
State(_resource_service): State<R>,
|
||||
depth: Option<Depth>,
|
||||
principal: R::Principal,
|
||||
overwrite: Overwrite,
|
||||
) -> Result<Response, R::Error> {
|
||||
// TODO: Actually implement, but to be WebDAV-compliant we must at least support this route but
|
||||
// can return a 403 error
|
||||
let _depth = depth.unwrap_or(Depth::Infinity);
|
||||
Ok(StatusCode::FORBIDDEN.into_response())
|
||||
}
|
||||
@@ -1,71 +1,91 @@
|
||||
use crate::Error;
|
||||
use crate::header::Depth;
|
||||
use crate::privileges::UserPrivilege;
|
||||
use crate::resource::PrincipalUri;
|
||||
use crate::resource::Resource;
|
||||
use crate::resource::ResourceName;
|
||||
use crate::resource::ResourceService;
|
||||
use crate::xml::MultistatusElement;
|
||||
use crate::xml::PropElement;
|
||||
use crate::xml::PropfindElement;
|
||||
use crate::xml::PropfindType;
|
||||
use actix_web::HttpRequest;
|
||||
use actix_web::web::Data;
|
||||
use actix_web::web::Path;
|
||||
use axum::extract::{Extension, OriginalUri, Path, State};
|
||||
use rustical_xml::PropName;
|
||||
use rustical_xml::XmlDocument;
|
||||
use tracing::instrument;
|
||||
use tracing_actix_web::RootSpan;
|
||||
|
||||
#[instrument(parent = root_span.id(), skip(path, req, root_span, resource_service))]
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub(crate) async fn route_propfind<R: ResourceService>(
|
||||
path: Path<R::PathComponents>,
|
||||
body: String,
|
||||
req: HttpRequest,
|
||||
user: R::Principal,
|
||||
type RSMultistatus<R> = MultistatusElement<
|
||||
<<R as ResourceService>::Resource as Resource>::Prop,
|
||||
<<R as ResourceService>::MemberType as Resource>::Prop,
|
||||
>;
|
||||
|
||||
#[instrument(skip(path, resource_service, puri))]
|
||||
pub(crate) async fn axum_route_propfind<R: ResourceService>(
|
||||
Path(path): Path<R::PathComponents>,
|
||||
State(resource_service): State<R>,
|
||||
depth: Depth,
|
||||
root_span: RootSpan,
|
||||
resource_service: Data<R>,
|
||||
) -> Result<
|
||||
MultistatusElement<<R::Resource as Resource>::Prop, <R::MemberType as Resource>::Prop>,
|
||||
R::Error,
|
||||
> {
|
||||
let resource = resource_service.get_resource(&path).await?;
|
||||
let privileges = resource.get_user_privileges(&user)?;
|
||||
principal: R::Principal,
|
||||
uri: OriginalUri,
|
||||
Extension(puri): Extension<R::PrincipalUri>,
|
||||
body: String,
|
||||
) -> Result<RSMultistatus<R>, R::Error> {
|
||||
route_propfind::<R>(
|
||||
&path,
|
||||
uri.path(),
|
||||
&body,
|
||||
&principal,
|
||||
&depth,
|
||||
&resource_service,
|
||||
&puri,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn route_propfind<R: ResourceService>(
|
||||
path_components: &R::PathComponents,
|
||||
path: &str,
|
||||
body: &str,
|
||||
principal: &R::Principal,
|
||||
depth: &Depth,
|
||||
resource_service: &R,
|
||||
puri: &impl PrincipalUri,
|
||||
) -> Result<RSMultistatus<R>, R::Error> {
|
||||
let resource = resource_service.get_resource(path_components).await?;
|
||||
let privileges = resource.get_user_privileges(principal)?;
|
||||
if !privileges.has(&UserPrivilege::Read) {
|
||||
return Err(Error::Unauthorized.into());
|
||||
}
|
||||
|
||||
// A request body is optional. If empty we MUST return all props
|
||||
let propfind: PropfindElement = if !body.is_empty() {
|
||||
PropfindElement::parse_str(&body).map_err(Error::XmlError)?
|
||||
} else {
|
||||
PropfindElement {
|
||||
prop: PropfindType::Allprop,
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: respect namespaces?
|
||||
let props = match &propfind.prop {
|
||||
PropfindType::Allprop => vec!["allprop"],
|
||||
PropfindType::Propname => vec!["propname"],
|
||||
PropfindType::Prop(PropElement(prop_tags)) => prop_tags
|
||||
.iter()
|
||||
.map(|propname| propname.0.as_str())
|
||||
.collect(),
|
||||
};
|
||||
let propfind_self: PropfindElement<<<R::Resource as Resource>::Prop as PropName>::Names> =
|
||||
if !body.is_empty() {
|
||||
PropfindElement::parse_str(body).map_err(Error::XmlError)?
|
||||
} else {
|
||||
PropfindElement {
|
||||
prop: PropfindType::Allprop,
|
||||
}
|
||||
};
|
||||
let propfind_member: PropfindElement<<<R::MemberType as Resource>::Prop as PropName>::Names> =
|
||||
if !body.is_empty() {
|
||||
PropfindElement::parse_str(body).map_err(Error::XmlError)?
|
||||
} else {
|
||||
PropfindElement {
|
||||
prop: PropfindType::Allprop,
|
||||
}
|
||||
};
|
||||
|
||||
let mut member_responses = Vec::new();
|
||||
if depth != Depth::Zero {
|
||||
for (subpath, member) in resource_service.get_members(&path).await? {
|
||||
if depth != &Depth::Zero {
|
||||
for member in resource_service.get_members(path_components).await? {
|
||||
member_responses.push(member.propfind(
|
||||
&format!("{}/{}", req.path().trim_end_matches('/'), subpath),
|
||||
&props,
|
||||
&user,
|
||||
req.resource_map(),
|
||||
&format!("{}/{}", path.trim_end_matches('/'), member.get_name()),
|
||||
&propfind_member.prop,
|
||||
puri,
|
||||
principal,
|
||||
)?);
|
||||
}
|
||||
}
|
||||
|
||||
let response = resource.propfind(req.path(), &props, &user, req.resource_map())?;
|
||||
let response = resource.propfind(path, &propfind_self.prop, puri, principal)?;
|
||||
|
||||
Ok(MultistatusElement {
|
||||
responses: vec![response],
|
||||
|
||||
@@ -5,18 +5,16 @@ use crate::resource::ResourceService;
|
||||
use crate::xml::MultistatusElement;
|
||||
use crate::xml::TagList;
|
||||
use crate::xml::multistatus::{PropstatElement, PropstatWrapper, ResponseElement};
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::web::Data;
|
||||
use actix_web::{HttpRequest, web::Path};
|
||||
use axum::extract::{OriginalUri, Path, State};
|
||||
use http::StatusCode;
|
||||
use quick_xml::name::Namespace;
|
||||
use rustical_xml::EnumUnitVariants;
|
||||
use rustical_xml::NamespaceOwned;
|
||||
use rustical_xml::PropName;
|
||||
use rustical_xml::Unparsed;
|
||||
use rustical_xml::XmlDeserialize;
|
||||
use rustical_xml::XmlDocument;
|
||||
use rustical_xml::XmlRootTag;
|
||||
use std::str::FromStr;
|
||||
use tracing::instrument;
|
||||
use tracing_actix_web::RootSpan;
|
||||
|
||||
#[derive(XmlDeserialize, Clone, Debug)]
|
||||
#[xml(untagged)]
|
||||
@@ -63,24 +61,32 @@ enum Operation<T: XmlDeserialize> {
|
||||
#[xml(ns = "crate::namespace::NS_DAV")]
|
||||
struct PropertyupdateElement<T: XmlDeserialize>(#[xml(ty = "untagged", flatten)] Vec<Operation<T>>);
|
||||
|
||||
#[instrument(parent = root_span.id(), skip(path, req, root_span, resource_service))]
|
||||
pub(crate) async fn route_proppatch<R: ResourceService>(
|
||||
path: Path<R::PathComponents>,
|
||||
body: String,
|
||||
req: HttpRequest,
|
||||
pub(crate) async fn axum_route_proppatch<R: ResourceService>(
|
||||
Path(path): Path<R::PathComponents>,
|
||||
State(resource_service): State<R>,
|
||||
principal: R::Principal,
|
||||
root_span: RootSpan,
|
||||
resource_service: Data<R>,
|
||||
uri: OriginalUri,
|
||||
body: String,
|
||||
) -> Result<MultistatusElement<String, String>, R::Error> {
|
||||
let href = req.path().to_owned();
|
||||
route_proppatch(&path, uri.path(), &body, &principal, &resource_service).await
|
||||
}
|
||||
|
||||
pub(crate) async fn route_proppatch<R: ResourceService>(
|
||||
path_components: &R::PathComponents,
|
||||
path: &str,
|
||||
body: &str,
|
||||
principal: &R::Principal,
|
||||
resource_service: &R,
|
||||
) -> Result<MultistatusElement<String, String>, R::Error> {
|
||||
let href = path.to_owned();
|
||||
|
||||
// Extract operations
|
||||
let PropertyupdateElement::<SetPropertyPropWrapperWrapper<<R::Resource as Resource>::Prop>>(
|
||||
operations,
|
||||
) = XmlDocument::parse_str(&body).map_err(Error::XmlError)?;
|
||||
) = XmlDocument::parse_str(body).map_err(Error::XmlError)?;
|
||||
|
||||
let mut resource = resource_service.get_resource(&path).await?;
|
||||
let privileges = resource.get_user_privileges(&principal)?;
|
||||
let mut resource = resource_service.get_resource(path_components).await?;
|
||||
let privileges = resource.get_user_privileges(principal)?;
|
||||
if !privileges.has(&UserPrivilege::Write) {
|
||||
return Err(Error::Unauthorized.into());
|
||||
}
|
||||
@@ -96,13 +102,15 @@ pub(crate) async fn route_proppatch<R: ResourceService>(
|
||||
}) => {
|
||||
match property {
|
||||
SetPropertyPropWrapper::Valid(prop) => {
|
||||
let propname: <<R::Resource as Resource>::Prop as EnumUnitVariants>::UnitVariants = prop.clone().into();
|
||||
let propname: <<R::Resource as Resource>::Prop as PropName>::Names =
|
||||
prop.clone().into();
|
||||
let (ns, propname): (Option<Namespace>, &str) = propname.into();
|
||||
match resource.set_prop(prop) {
|
||||
Ok(()) => props_ok.push((ns, propname.to_owned())),
|
||||
Err(Error::PropReadOnly) => {
|
||||
props_conflict.push((ns, propname.to_owned()))
|
||||
Ok(()) => {
|
||||
props_ok.push((ns.map(NamespaceOwned::from), propname.to_owned()))
|
||||
}
|
||||
Err(Error::PropReadOnly) => props_conflict
|
||||
.push((ns.map(NamespaceOwned::from), propname.to_owned())),
|
||||
Err(err) => return Err(err.into()),
|
||||
};
|
||||
}
|
||||
@@ -113,7 +121,7 @@ pub(crate) async fn route_proppatch<R: ResourceService>(
|
||||
.into_iter()
|
||||
.find_map(|(ns, tag)| {
|
||||
if tag == propname.as_str() {
|
||||
Some((ns, tag.to_owned()))
|
||||
Some((ns.map(NamespaceOwned::from), tag.to_owned()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -131,14 +139,12 @@ pub(crate) async fn route_proppatch<R: ResourceService>(
|
||||
}
|
||||
Operation::Remove(remove_el) => {
|
||||
let propname = remove_el.prop.0.0;
|
||||
match <<R::Resource as Resource>::Prop as EnumUnitVariants>::UnitVariants::from_str(
|
||||
&propname,
|
||||
) {
|
||||
match <<R::Resource as Resource>::Prop as PropName>::Names::from_str(&propname) {
|
||||
Ok(prop) => match resource.remove_prop(&prop) {
|
||||
Ok(()) => props_ok.push((None, propname)),
|
||||
Err(Error::PropReadOnly) => props_conflict.push({
|
||||
let (ns, tag) = prop.into();
|
||||
(ns, tag.to_owned())
|
||||
(ns.map(NamespaceOwned::from), tag.to_owned())
|
||||
}),
|
||||
Err(err) => return Err(err.into()),
|
||||
},
|
||||
@@ -151,7 +157,9 @@ pub(crate) async fn route_proppatch<R: ResourceService>(
|
||||
|
||||
if props_not_found.is_empty() && props_conflict.is_empty() {
|
||||
// Only save if no errors occured
|
||||
resource_service.save_resource(&path, resource).await?;
|
||||
resource_service
|
||||
.save_resource(path_components, resource)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(MultistatusElement {
|
||||
|
||||
@@ -1,21 +1,26 @@
|
||||
use crate::Principal;
|
||||
use crate::privileges::UserPrivilegeSet;
|
||||
use crate::xml::Resourcetype;
|
||||
use crate::xml::multistatus::{PropTagWrapper, PropstatElement, PropstatWrapper};
|
||||
use crate::xml::{PropElement, PropfindType, Resourcetype};
|
||||
use crate::xml::{TagList, multistatus::ResponseElement};
|
||||
use crate::{Error, Principal};
|
||||
use actix_web::dev::ResourceMap;
|
||||
use actix_web::http::header::{EntityTag, IfMatch, IfNoneMatch};
|
||||
use actix_web::{ResponseError, http::StatusCode};
|
||||
use headers::{ETag, IfMatch, IfNoneMatch};
|
||||
use http::StatusCode;
|
||||
use itertools::Itertools;
|
||||
use quick_xml::name::Namespace;
|
||||
pub use resource_service::ResourceService;
|
||||
use rustical_xml::{EnumUnitVariants, EnumVariants, XmlDeserialize, XmlSerialize};
|
||||
use rustical_xml::{EnumVariants, NamespaceOwned, PropName, XmlDeserialize, XmlSerialize};
|
||||
use std::collections::HashSet;
|
||||
use std::str::FromStr;
|
||||
|
||||
mod axum_methods;
|
||||
mod axum_service;
|
||||
mod methods;
|
||||
mod principal_uri;
|
||||
mod resource_service;
|
||||
|
||||
pub use resource_service::*;
|
||||
pub use axum_methods::AxumMethods;
|
||||
pub use axum_service::AxumService;
|
||||
pub use principal_uri::PrincipalUri;
|
||||
|
||||
pub trait ResourceProp: XmlSerialize + XmlDeserialize {}
|
||||
impl<T: XmlSerialize + XmlDeserialize> ResourceProp for T {}
|
||||
@@ -23,11 +28,17 @@ impl<T: XmlSerialize + XmlDeserialize> ResourceProp for T {}
|
||||
pub trait ResourcePropName: FromStr {}
|
||||
impl<T: FromStr> ResourcePropName for T {}
|
||||
|
||||
pub trait Resource: Clone + 'static {
|
||||
type Prop: ResourceProp + PartialEq + Clone + EnumVariants + EnumUnitVariants;
|
||||
type Error: ResponseError + From<crate::Error>;
|
||||
pub trait ResourceName {
|
||||
fn get_name(&self) -> String;
|
||||
}
|
||||
|
||||
pub trait Resource: Clone + Send + 'static {
|
||||
type Prop: ResourceProp + PartialEq + Clone + EnumVariants + PropName + Send;
|
||||
type Error: From<crate::Error>;
|
||||
type Principal: Principal;
|
||||
|
||||
const IS_COLLECTION: bool;
|
||||
|
||||
fn get_resourcetype(&self) -> Resourcetype;
|
||||
|
||||
fn list_props() -> Vec<(Option<Namespace<'static>>, &'static str)> {
|
||||
@@ -36,19 +47,16 @@ pub trait Resource: Clone + 'static {
|
||||
|
||||
fn get_prop(
|
||||
&self,
|
||||
rmap: &ResourceMap,
|
||||
principal_uri: &impl PrincipalUri,
|
||||
principal: &Self::Principal,
|
||||
prop: &<Self::Prop as EnumUnitVariants>::UnitVariants,
|
||||
prop: &<Self::Prop as PropName>::Names,
|
||||
) -> Result<Self::Prop, Self::Error>;
|
||||
|
||||
fn set_prop(&mut self, _prop: Self::Prop) -> Result<(), crate::Error> {
|
||||
Err(crate::Error::PropReadOnly)
|
||||
}
|
||||
|
||||
fn remove_prop(
|
||||
&mut self,
|
||||
_prop: &<Self::Prop as EnumUnitVariants>::UnitVariants,
|
||||
) -> Result<(), crate::Error> {
|
||||
fn remove_prop(&mut self, _prop: &<Self::Prop as PropName>::Names) -> Result<(), crate::Error> {
|
||||
Err(crate::Error::PropReadOnly)
|
||||
}
|
||||
|
||||
@@ -61,34 +69,26 @@ pub trait Resource: Clone + 'static {
|
||||
}
|
||||
|
||||
fn satisfies_if_match(&self, if_match: &IfMatch) -> bool {
|
||||
match if_match {
|
||||
IfMatch::Any => true,
|
||||
// This is not nice but if the header doesn't exist, actix just gives us an empty
|
||||
// IfMatch::Items header
|
||||
IfMatch::Items(items) if items.is_empty() => true,
|
||||
IfMatch::Items(items) => {
|
||||
if let Some(etag) = self.get_etag() {
|
||||
let etag = EntityTag::new_strong(etag.to_owned());
|
||||
return items.iter().any(|item| item.strong_eq(&etag));
|
||||
}
|
||||
false
|
||||
if let Some(etag) = self.get_etag() {
|
||||
if let Ok(etag) = ETag::from_str(&etag) {
|
||||
if_match.precondition_passes(&etag)
|
||||
} else {
|
||||
if_match.is_any()
|
||||
}
|
||||
} else {
|
||||
if_match.is_any()
|
||||
}
|
||||
}
|
||||
|
||||
fn satisfies_if_none_match(&self, if_none_match: &IfNoneMatch) -> bool {
|
||||
match if_none_match {
|
||||
IfNoneMatch::Any => false,
|
||||
// This is not nice but if the header doesn't exist, actix just gives us an empty
|
||||
// IfNoneMatch::Items header
|
||||
IfNoneMatch::Items(items) if items.is_empty() => false,
|
||||
IfNoneMatch::Items(items) => {
|
||||
if let Some(etag) = self.get_etag() {
|
||||
let etag = EntityTag::new_strong(etag.to_owned());
|
||||
return items.iter().all(|item| item.strong_ne(&etag));
|
||||
}
|
||||
true
|
||||
if let Some(etag) = self.get_etag() {
|
||||
if let Ok(etag) = ETag::from_str(&etag) {
|
||||
if_none_match.precondition_passes(&etag)
|
||||
} else {
|
||||
if_none_match != &IfNoneMatch::any()
|
||||
}
|
||||
} else {
|
||||
if_none_match != &IfNoneMatch::any()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,61 +100,50 @@ pub trait Resource: Clone + 'static {
|
||||
fn propfind(
|
||||
&self,
|
||||
path: &str,
|
||||
props: &[&str],
|
||||
prop: &PropfindType<<Self::Prop as PropName>::Names>,
|
||||
principal_uri: &impl PrincipalUri,
|
||||
principal: &Self::Principal,
|
||||
rmap: &ResourceMap,
|
||||
) -> Result<ResponseElement<Self::Prop>, Self::Error> {
|
||||
let mut props = props.to_vec();
|
||||
|
||||
if props.contains(&"propname") {
|
||||
if props.len() != 1 {
|
||||
// propname MUST be the only queried prop per spec
|
||||
return Err(
|
||||
Error::BadRequest("propname MUST be the only queried prop".to_owned()).into(),
|
||||
);
|
||||
}
|
||||
|
||||
let props = Self::list_props()
|
||||
.into_iter()
|
||||
.map(|(ns, tag)| (ns.to_owned(), tag.to_string()))
|
||||
.collect_vec();
|
||||
|
||||
return Ok(ResponseElement {
|
||||
href: path.to_owned(),
|
||||
propstat: vec![PropstatWrapper::TagList(PropstatElement {
|
||||
prop: TagList::from(props),
|
||||
status: StatusCode::OK,
|
||||
})],
|
||||
..Default::default()
|
||||
});
|
||||
// Collections have a trailing slash
|
||||
let mut path = path.to_string();
|
||||
if Self::IS_COLLECTION && !path.ends_with('/') {
|
||||
path.push('/');
|
||||
}
|
||||
|
||||
if props.contains(&"allprop") {
|
||||
if props.len() != 1 {
|
||||
// allprop MUST be the only queried prop per spec
|
||||
return Err(
|
||||
Error::BadRequest("allprop MUST be the only queried prop".to_owned()).into(),
|
||||
);
|
||||
}
|
||||
props = Self::list_props()
|
||||
.into_iter()
|
||||
.map(|(_ns, tag)| tag)
|
||||
.collect();
|
||||
}
|
||||
// TODO: Support include element
|
||||
let (props, invalid_props): (HashSet<<Self::Prop as PropName>::Names>, Vec<_>) = match prop
|
||||
{
|
||||
PropfindType::Propname => {
|
||||
let props = Self::list_props()
|
||||
.into_iter()
|
||||
.map(|(ns, tag)| (ns.map(NamespaceOwned::from), tag.to_string()))
|
||||
.collect_vec();
|
||||
|
||||
let mut valid_props = vec![];
|
||||
let mut invalid_props = vec![];
|
||||
for prop in props {
|
||||
if let Ok(valid_prop) = <Self::Prop as EnumUnitVariants>::UnitVariants::from_str(prop) {
|
||||
valid_props.push(valid_prop);
|
||||
} else {
|
||||
invalid_props.push(prop.to_string())
|
||||
return Ok(ResponseElement {
|
||||
href: path.to_owned(),
|
||||
propstat: vec![PropstatWrapper::TagList(PropstatElement {
|
||||
prop: TagList::from(props),
|
||||
status: StatusCode::OK,
|
||||
})],
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
}
|
||||
PropfindType::Allprop => (
|
||||
Self::list_props()
|
||||
.iter()
|
||||
.map(|(_ns, name)| <Self::Prop as PropName>::Names::from_str(name).unwrap())
|
||||
.collect(),
|
||||
vec![],
|
||||
),
|
||||
PropfindType::Prop(PropElement(valid_tags, invalid_tags)) => (
|
||||
valid_tags.iter().cloned().collect(),
|
||||
invalid_tags.to_owned(),
|
||||
),
|
||||
};
|
||||
|
||||
let prop_responses = valid_props
|
||||
let prop_responses = props
|
||||
.into_iter()
|
||||
.map(|prop| self.get_prop(rmap, principal, &prop))
|
||||
.map(|prop| self.get_prop(principal_uri, principal, &prop))
|
||||
.collect::<Result<Vec<_>, Self::Error>>()?;
|
||||
|
||||
let mut propstats = vec![PropstatWrapper::Normal(PropstatElement {
|
||||
@@ -164,11 +153,7 @@ pub trait Resource: Clone + 'static {
|
||||
if !invalid_props.is_empty() {
|
||||
propstats.push(PropstatWrapper::TagList(PropstatElement {
|
||||
status: StatusCode::NOT_FOUND,
|
||||
prop: invalid_props
|
||||
.into_iter()
|
||||
.map(|tag| (None, tag))
|
||||
.collect_vec()
|
||||
.into(),
|
||||
prop: invalid_props.into(),
|
||||
}));
|
||||
}
|
||||
Ok(ResponseElement {
|
||||
|
||||
3
crates/dav/src/resource/principal_uri.rs
Normal file
3
crates/dav/src/resource/principal_uri.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub trait PrincipalUri: 'static + Clone + Send + Sync {
|
||||
fn principal_uri(&self, principal: &str) -> String;
|
||||
}
|
||||
@@ -1,29 +1,28 @@
|
||||
use actix_web::dev::{AppService, HttpServiceFactory};
|
||||
use actix_web::error::UrlGenerationError;
|
||||
use actix_web::test::TestRequest;
|
||||
use actix_web::web::Data;
|
||||
use actix_web::{ResponseError, dev::ResourceMap, http::Method, web};
|
||||
use async_trait::async_trait;
|
||||
use serde::Deserialize;
|
||||
use std::str::FromStr;
|
||||
|
||||
use super::{PrincipalUri, Resource};
|
||||
use crate::Principal;
|
||||
use crate::resource::{AxumMethods, AxumService};
|
||||
use async_trait::async_trait;
|
||||
use axum::Router;
|
||||
use axum::extract::FromRequestParts;
|
||||
use axum::response::IntoResponse;
|
||||
use serde::Deserialize;
|
||||
|
||||
use super::Resource;
|
||||
use super::methods::{route_delete, route_propfind, route_proppatch};
|
||||
|
||||
#[async_trait(?Send)]
|
||||
pub trait ResourceService: Sized + 'static {
|
||||
type MemberType: Resource<Error = Self::Error, Principal = Self::Principal>;
|
||||
type PathComponents: for<'de> Deserialize<'de> + Sized + Clone + 'static; // defines how the resource URI maps to parameters, i.e. /{principal}/{calendar} -> (String, String)
|
||||
#[async_trait]
|
||||
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 MemberType: Resource<Error = Self::Error, Principal = Self::Principal>
|
||||
+ super::ResourceName;
|
||||
type Resource: Resource<Error = Self::Error, Principal = Self::Principal>;
|
||||
type Error: ResponseError + From<crate::Error>;
|
||||
type Principal: Principal;
|
||||
type Error: From<crate::Error> + Send + Sync + IntoResponse + 'static;
|
||||
type Principal: Principal + FromRequestParts<Self>;
|
||||
type PrincipalUri: PrincipalUri;
|
||||
|
||||
const DAV_HEADER: &'static str;
|
||||
|
||||
async fn get_members(
|
||||
&self,
|
||||
_path_components: &Self::PathComponents,
|
||||
) -> Result<Vec<(String, Self::MemberType)>, Self::Error> {
|
||||
_path: &Self::PathComponents,
|
||||
) -> Result<Vec<Self::MemberType>, Self::Error> {
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
@@ -31,6 +30,7 @@ pub trait ResourceService: Sized + 'static {
|
||||
&self,
|
||||
_path: &Self::PathComponents,
|
||||
) -> Result<Self::Resource, Self::Error>;
|
||||
|
||||
async fn save_resource(
|
||||
&self,
|
||||
_path: &Self::PathComponents,
|
||||
@@ -38,6 +38,7 @@ pub trait ResourceService: Sized + 'static {
|
||||
) -> Result<(), Self::Error> {
|
||||
Err(crate::Error::Unauthorized.into())
|
||||
}
|
||||
|
||||
async fn delete_resource(
|
||||
&self,
|
||||
_path: &Self::PathComponents,
|
||||
@@ -46,51 +47,14 @@ pub trait ResourceService: Sized + 'static {
|
||||
Err(crate::Error::Unauthorized.into())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn actix_resource(self) -> actix_web::Resource {
|
||||
Self::actix_additional_routes(
|
||||
web::resource("")
|
||||
.app_data(Data::new(self))
|
||||
.route(
|
||||
web::method(Method::from_str("PROPFIND").unwrap()).to(route_propfind::<Self>),
|
||||
)
|
||||
.route(
|
||||
web::method(Method::from_str("PROPPATCH").unwrap()).to(route_proppatch::<Self>),
|
||||
)
|
||||
.delete(route_delete::<Self>),
|
||||
)
|
||||
}
|
||||
|
||||
/// Hook for other resources to insert their additional methods (i.e. REPORT, MKCALENDAR)
|
||||
#[inline]
|
||||
fn actix_additional_routes(res: actix_web::Resource) -> actix_web::Resource {
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
pub trait NamedRoute {
|
||||
fn route_name() -> &'static str;
|
||||
|
||||
fn get_url<U, I>(rmap: &ResourceMap, elements: U) -> Result<String, UrlGenerationError>
|
||||
fn axum_service(self) -> AxumService<Self>
|
||||
where
|
||||
U: IntoIterator<Item = I>,
|
||||
I: AsRef<str>,
|
||||
Self: AxumMethods,
|
||||
{
|
||||
Ok(rmap
|
||||
.url_for(
|
||||
&TestRequest::default().to_http_request(),
|
||||
Self::route_name(),
|
||||
elements,
|
||||
)?
|
||||
.path()
|
||||
.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ResourceServiceRoute<RS: ResourceService>(pub RS);
|
||||
|
||||
impl<RS: ResourceService> HttpServiceFactory for ResourceServiceRoute<RS> {
|
||||
fn register(self, config: &mut AppService) {
|
||||
self.0.actix_resource().register(config);
|
||||
AxumService::new(self)
|
||||
}
|
||||
|
||||
fn axum_router<S: Send + Sync + Clone + 'static>(self) -> Router<S> {
|
||||
Router::new().route_service("/", self.axum_service())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,11 @@ use crate::extensions::{
|
||||
CommonPropertiesExtension, CommonPropertiesProp, CommonPropertiesPropName,
|
||||
};
|
||||
use crate::privileges::UserPrivilegeSet;
|
||||
use crate::resource::{NamedRoute, Resource, ResourceService};
|
||||
use crate::resource::{AxumMethods, PrincipalUri, Resource, ResourceName, ResourceService};
|
||||
use crate::xml::{Resourcetype, ResourcetypeInner};
|
||||
use actix_web::dev::ResourceMap;
|
||||
use async_trait::async_trait;
|
||||
use axum::Router;
|
||||
use axum::extract::FromRequestParts;
|
||||
use std::marker::PhantomData;
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -18,15 +19,13 @@ impl<PR: Resource, P: Principal> Default for RootResource<PR, P> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<PR: Resource + NamedRoute, P: Principal> CommonPropertiesExtension for RootResource<PR, P> {
|
||||
type PrincipalResource = PR;
|
||||
}
|
||||
|
||||
impl<PR: Resource + NamedRoute, P: Principal> Resource for RootResource<PR, P> {
|
||||
impl<PR: Resource, P: Principal> Resource for RootResource<PR, P> {
|
||||
type Prop = CommonPropertiesProp;
|
||||
type Error = PR::Error;
|
||||
type Principal = P;
|
||||
|
||||
const IS_COLLECTION: bool = true;
|
||||
|
||||
fn get_resourcetype(&self) -> Resourcetype {
|
||||
Resourcetype(&[ResourcetypeInner(
|
||||
Some(crate::namespace::NS_DAV),
|
||||
@@ -36,11 +35,11 @@ impl<PR: Resource + NamedRoute, P: Principal> Resource for RootResource<PR, P> {
|
||||
|
||||
fn get_prop(
|
||||
&self,
|
||||
rmap: &ResourceMap,
|
||||
principal_uri: &impl PrincipalUri,
|
||||
user: &P,
|
||||
prop: &CommonPropertiesPropName,
|
||||
) -> Result<Self::Prop, Self::Error> {
|
||||
CommonPropertiesExtension::get_prop(self, rmap, user, prop)
|
||||
CommonPropertiesExtension::get_prop(self, principal_uri, user, prop)
|
||||
}
|
||||
|
||||
fn get_user_privileges(&self, _user: &P) -> Result<UserPrivilegeSet, Self::Error> {
|
||||
@@ -49,25 +48,50 @@ impl<PR: Resource + NamedRoute, P: Principal> Resource for RootResource<PR, P> {
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RootResourceService<PR: Resource, P: Principal>(PhantomData<PR>, PhantomData<P>);
|
||||
pub struct RootResourceService<PRS: ResourceService + Clone, P: Principal, PURI: PrincipalUri>(
|
||||
PRS,
|
||||
PhantomData<P>,
|
||||
PhantomData<PURI>,
|
||||
);
|
||||
|
||||
impl<PR: Resource, P: Principal> Default for RootResourceService<PR, P> {
|
||||
fn default() -> Self {
|
||||
Self(PhantomData, PhantomData)
|
||||
impl<PRS: ResourceService + Clone, P: Principal, PURI: PrincipalUri>
|
||||
RootResourceService<PRS, P, PURI>
|
||||
{
|
||||
pub fn new(principal_resource_service: PRS) -> Self {
|
||||
Self(principal_resource_service, PhantomData, PhantomData)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl<PR: Resource<Principal = P> + NamedRoute, P: Principal> ResourceService
|
||||
for RootResourceService<PR, P>
|
||||
#[async_trait]
|
||||
impl<
|
||||
PRS: ResourceService<Principal = P> + Clone,
|
||||
P: Principal + FromRequestParts<Self>,
|
||||
PURI: PrincipalUri,
|
||||
> ResourceService for RootResourceService<PRS, P, PURI>
|
||||
where
|
||||
PRS::Resource: ResourceName,
|
||||
{
|
||||
type PathComponents = ();
|
||||
type MemberType = PR;
|
||||
type Resource = RootResource<PR, P>;
|
||||
type Error = PR::Error;
|
||||
type MemberType = PRS::Resource;
|
||||
type Resource = RootResource<PRS::Resource, P>;
|
||||
type Error = PRS::Error;
|
||||
type Principal = P;
|
||||
type PrincipalUri = PURI;
|
||||
|
||||
const DAV_HEADER: &str = "1, 3, access-control";
|
||||
|
||||
async fn get_resource(&self, _: &()) -> Result<Self::Resource, Self::Error> {
|
||||
Ok(RootResource::<PR, P>::default())
|
||||
Ok(RootResource::<PRS::Resource, P>::default())
|
||||
}
|
||||
|
||||
fn axum_router<S: Send + Sync + Clone + 'static>(self) -> Router<S> {
|
||||
Router::new()
|
||||
.nest("/principal/{principal}", self.0.clone().axum_router())
|
||||
.route_service("/", self.axum_service())
|
||||
}
|
||||
}
|
||||
|
||||
impl<PRS: ResourceService<Principal = P> + Clone, P: Principal, PURI: PrincipalUri> AxumMethods
|
||||
for RootResourceService<PRS, P, PURI>
|
||||
{
|
||||
}
|
||||
|
||||
12
crates/dav/src/xml/error.rs
Normal file
12
crates/dav/src/xml/error.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
use rustical_xml::{XmlRootTag, XmlSerialize};
|
||||
|
||||
#[derive(XmlSerialize, XmlRootTag)]
|
||||
#[xml(ns = "crate::namespace::NS_DAV", root = b"error")]
|
||||
#[xml(ns_prefix(
|
||||
crate::namespace::NS_DAV = b"",
|
||||
crate::namespace::NS_CARDDAV = b"CARD",
|
||||
crate::namespace::NS_CALDAV = b"CAL",
|
||||
crate::namespace::NS_CALENDARSERVER = b"CS",
|
||||
crate::namespace::NS_DAVPUSH = b"PUSH"
|
||||
))]
|
||||
pub struct ErrorElement<'t, T: XmlSerialize>(#[xml(ty = "untagged")] pub &'t T);
|
||||
14
crates/dav/src/xml/href.rs
Normal file
14
crates/dav/src/xml/href.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
use derive_more::From;
|
||||
use rustical_xml::{XmlDeserialize, XmlSerialize};
|
||||
|
||||
#[derive(XmlDeserialize, XmlSerialize, Debug, Clone, From, PartialEq)]
|
||||
pub struct HrefElement {
|
||||
#[xml(ns = "crate::namespace::NS_DAV")]
|
||||
pub href: String,
|
||||
}
|
||||
|
||||
impl HrefElement {
|
||||
pub fn new(href: String) -> Self {
|
||||
Self { href }
|
||||
}
|
||||
}
|
||||
@@ -2,22 +2,12 @@ pub mod multistatus;
|
||||
mod propfind;
|
||||
mod resourcetype;
|
||||
pub mod tag_list;
|
||||
use derive_more::derive::From;
|
||||
pub use multistatus::MultistatusElement;
|
||||
pub use propfind::{PropElement, PropfindElement, PropfindType, Propname};
|
||||
mod href;
|
||||
pub use href::HrefElement;
|
||||
pub use propfind::{PropElement, PropfindElement, PropfindType};
|
||||
pub use resourcetype::{Resourcetype, ResourcetypeInner};
|
||||
use rustical_xml::{XmlDeserialize, XmlSerialize};
|
||||
pub use tag_list::TagList;
|
||||
mod error;
|
||||
pub mod sync_collection;
|
||||
|
||||
#[derive(XmlDeserialize, XmlSerialize, Debug, Clone, From, PartialEq)]
|
||||
pub struct HrefElement {
|
||||
#[xml(ns = "crate::namespace::NS_DAV")]
|
||||
pub href: String,
|
||||
}
|
||||
|
||||
impl HrefElement {
|
||||
pub fn new(href: String) -> Self {
|
||||
Self { href }
|
||||
}
|
||||
}
|
||||
pub use error::ErrorElement;
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::xml::TagList;
|
||||
use actix_web::{
|
||||
body::BoxBody,
|
||||
http::{header::ContentType, StatusCode},
|
||||
HttpRequest, HttpResponse, Responder, ResponseError,
|
||||
};
|
||||
use http::StatusCode;
|
||||
use quick_xml::name::Namespace;
|
||||
use rustical_xml::{XmlRootTag, XmlSerialize, XmlSerializeRoot};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(XmlSerialize)]
|
||||
pub struct PropTagWrapper<T: XmlSerialize>(#[xml(flatten, ty = "untagged")] pub Vec<T>);
|
||||
@@ -109,18 +104,23 @@ impl<T1: XmlSerialize, T2: XmlSerialize> Default for MultistatusElement<T1, T2>
|
||||
}
|
||||
}
|
||||
|
||||
impl<T1: XmlSerialize, T2: XmlSerialize> Responder for MultistatusElement<T1, T2> {
|
||||
type Body = BoxBody;
|
||||
impl<T1: XmlSerialize, T2: XmlSerialize> axum::response::IntoResponse
|
||||
for MultistatusElement<T1, T2>
|
||||
{
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
use axum::body::Body;
|
||||
use http::header;
|
||||
|
||||
fn respond_to(self, _req: &HttpRequest) -> HttpResponse<Self::Body> {
|
||||
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) = self.serialize_root(&mut writer) {
|
||||
return crate::Error::from(err).error_response();
|
||||
return crate::Error::from(err).into_response();
|
||||
}
|
||||
|
||||
HttpResponse::MultiStatus()
|
||||
.content_type(ContentType::xml())
|
||||
.body(String::from_utf8(output).unwrap())
|
||||
let mut resp = axum::response::Response::builder().status(StatusCode::MULTI_STATUS);
|
||||
resp.headers_mut()
|
||||
.unwrap()
|
||||
.insert(header::CONTENT_TYPE, "application/xml".try_into().unwrap());
|
||||
resp.body(Body::from(output)).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,85 @@
|
||||
use quick_xml::events::Event;
|
||||
use quick_xml::name::ResolveResult;
|
||||
use rustical_xml::NamespaceOwned;
|
||||
use rustical_xml::Unparsed;
|
||||
use rustical_xml::XmlDeserialize;
|
||||
use rustical_xml::XmlError;
|
||||
use rustical_xml::XmlRootTag;
|
||||
|
||||
#[derive(Debug, Clone, XmlDeserialize, XmlRootTag, PartialEq)]
|
||||
#[xml(root = b"propfind", ns = "crate::namespace::NS_DAV")]
|
||||
pub struct PropfindElement {
|
||||
pub struct PropfindElement<PN: XmlDeserialize> {
|
||||
#[xml(ty = "untagged")]
|
||||
pub prop: PropfindType,
|
||||
pub prop: PropfindType<PN>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
// pub struct PropElement<PN: XmlDeserialize = Propname>(#[xml(ty = "untagged", flatten)] pub Vec<PN>);
|
||||
pub struct PropElement<PN: XmlDeserialize>(
|
||||
// valid
|
||||
pub Vec<PN>,
|
||||
// invalid
|
||||
pub Vec<(Option<NamespaceOwned>, String)>,
|
||||
);
|
||||
|
||||
impl<PN: XmlDeserialize> XmlDeserialize for PropElement<PN> {
|
||||
fn deserialize<R: std::io::BufRead>(
|
||||
reader: &mut quick_xml::NsReader<R>,
|
||||
start: &quick_xml::events::BytesStart,
|
||||
empty: bool,
|
||||
) -> Result<Self, XmlError> {
|
||||
if empty {
|
||||
return Ok(Self(vec![], vec![]));
|
||||
}
|
||||
let mut buf = Vec::new();
|
||||
let mut valid_props = vec![];
|
||||
let mut invalid_props = vec![];
|
||||
loop {
|
||||
let event = reader.read_event_into(&mut buf)?;
|
||||
match &event {
|
||||
Event::End(e) if e.name() == start.name() => {
|
||||
break;
|
||||
}
|
||||
Event::Eof => return Err(XmlError::Eof),
|
||||
// start of a child element
|
||||
Event::Start(start) | Event::Empty(start) => {
|
||||
let empty = matches!(event, Event::Empty(_));
|
||||
let (ns, name) = reader.resolve_element(start.name());
|
||||
let ns = match ns {
|
||||
ResolveResult::Bound(ns) => Some(NamespaceOwned::from(ns)),
|
||||
ResolveResult::Unknown(_ns) => todo!("handle error"),
|
||||
ResolveResult::Unbound => None,
|
||||
};
|
||||
|
||||
match PN::deserialize(reader, start, empty) {
|
||||
Ok(propname) => valid_props.push(propname),
|
||||
Err(XmlError::InvalidVariant(_)) => {
|
||||
invalid_props
|
||||
.push((ns, String::from_utf8_lossy(name.as_ref()).to_string()));
|
||||
// Consume content
|
||||
Unparsed::deserialize(reader, start, empty)?;
|
||||
}
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
}
|
||||
Event::Text(_) | Event::CData(_) => {
|
||||
return Err(XmlError::UnsupportedEvent("Not expecting text here"));
|
||||
}
|
||||
Event::Decl(_) | Event::Comment(_) | Event::DocType(_) | Event::PI(_) => { /* ignore */
|
||||
}
|
||||
Event::End(_end) => {
|
||||
unreachable!(
|
||||
"Unexpected closing tag for wrong element, should be handled by quick_xml"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Self(valid_props, invalid_props))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, XmlDeserialize, PartialEq)]
|
||||
pub struct PropElement<PN: XmlDeserialize = Propname>(#[xml(ty = "untagged", flatten)] pub Vec<PN>);
|
||||
|
||||
#[derive(Debug, Clone, XmlDeserialize, PartialEq)]
|
||||
pub struct Propname(#[xml(ty = "tag_name")] pub String);
|
||||
|
||||
#[derive(Debug, Clone, XmlDeserialize, PartialEq)]
|
||||
pub enum PropfindType<PN: XmlDeserialize = Propname> {
|
||||
pub enum PropfindType<PN: XmlDeserialize> {
|
||||
#[xml(ns = "crate::namespace::NS_DAV")]
|
||||
Propname,
|
||||
#[xml(ns = "crate::namespace::NS_DAV")]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use rustical_xml::{ValueDeserialize, ValueSerialize, XmlDeserialize};
|
||||
|
||||
use super::{PropfindType, Propname};
|
||||
use super::PropfindType;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum SyncLevel {
|
||||
@@ -37,7 +37,7 @@ impl ValueSerialize for SyncLevel {
|
||||
// <!-- DAV:limit defined in RFC 5323, Section 5.17 -->
|
||||
// <!-- DAV:prop defined in RFC 4918, Section 14.18 -->
|
||||
#[xml(ns = "crate::namespace::NS_DAV")]
|
||||
pub struct SyncCollectionRequest<PN: XmlDeserialize = Propname> {
|
||||
pub struct SyncCollectionRequest<PN: XmlDeserialize> {
|
||||
#[xml(ns = "crate::namespace::NS_DAV")]
|
||||
pub sync_token: String,
|
||||
#[xml(ns = "crate::namespace::NS_DAV")]
|
||||
|
||||
@@ -1,35 +1,27 @@
|
||||
use derive_more::derive::From;
|
||||
use quick_xml::name::Namespace;
|
||||
use rustical_xml::XmlSerialize;
|
||||
use rustical_xml::{NamespaceOwned, XmlSerialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, From)]
|
||||
pub struct TagList(Vec<(Option<Namespace<'static>>, String)>);
|
||||
pub struct TagList(Vec<(Option<NamespaceOwned>, String)>);
|
||||
|
||||
impl XmlSerialize for TagList {
|
||||
fn serialize<W: std::io::Write>(
|
||||
&self,
|
||||
ns: Option<Namespace>,
|
||||
tag: Option<&[u8]>,
|
||||
namespaces: &HashMap<Namespace, &[u8]>,
|
||||
_ns: Option<Namespace>,
|
||||
_tag: Option<&[u8]>,
|
||||
_namespaces: &HashMap<Namespace, &[u8]>,
|
||||
writer: &mut quick_xml::Writer<W>,
|
||||
) -> std::io::Result<()> {
|
||||
#[derive(Debug, XmlSerialize, PartialEq)]
|
||||
struct Inner(#[xml(ty = "untagged", flatten)] Vec<Tag>);
|
||||
|
||||
#[derive(Debug, XmlSerialize, PartialEq)]
|
||||
struct Tag(
|
||||
#[xml(ty = "namespace")] Option<Namespace<'static>>,
|
||||
#[xml(ty = "tag_name")] String,
|
||||
);
|
||||
|
||||
Inner(
|
||||
self.0
|
||||
.iter()
|
||||
.map(|(ns, tag)| Tag(ns.to_owned(), tag.to_owned()))
|
||||
.collect(),
|
||||
)
|
||||
.serialize(ns, tag, namespaces, writer)
|
||||
for (ns, tag) in &self.0 {
|
||||
let mut el = writer.create_element(tag);
|
||||
if let Some(ns) = ns {
|
||||
el = el.with_attribute(("xmlns", String::from_utf8_lossy(&ns.0)));
|
||||
}
|
||||
el.write_empty()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(refining_impl_trait)]
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
use rustical_dav::xml::{PropElement, PropfindElement, PropfindType, Propname};
|
||||
use rustical_xml::de::XmlDocument;
|
||||
|
||||
#[test]
|
||||
fn propfind_allprop() {
|
||||
let propfind = PropfindElement::parse_str(
|
||||
r#"
|
||||
<propfind xmlns="DAV:">
|
||||
<allprop />
|
||||
</propfind>
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
propfind,
|
||||
PropfindElement {
|
||||
prop: PropfindType::Allprop
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn propfind_propname() {
|
||||
let propfind = PropfindElement::parse_str(
|
||||
r#"
|
||||
<propfind xmlns="DAV:">
|
||||
<propname />
|
||||
</propfind>
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
propfind,
|
||||
PropfindElement {
|
||||
prop: PropfindType::Propname
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn propfind_prop() {
|
||||
let propfind = PropfindElement::parse_str(
|
||||
r#"
|
||||
<propfind xmlns="DAV:">
|
||||
<prop>
|
||||
<displayname />
|
||||
<color />
|
||||
</prop>
|
||||
</propfind>
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
propfind,
|
||||
PropfindElement {
|
||||
prop: PropfindType::Prop(PropElement(vec![
|
||||
Propname("displayname".to_owned()),
|
||||
Propname("color".to_owned()),
|
||||
]))
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/// Example taken from DAVx5
|
||||
#[test]
|
||||
fn propfind_decl() {
|
||||
let propfind = PropfindElement::parse_str(
|
||||
r#"
|
||||
<?xml version='1.0' encoding='UTF-8' ?>
|
||||
<propfind xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav">
|
||||
<prop>
|
||||
<CARD:max-resource-size />
|
||||
<CARD:supported-address-data />
|
||||
<supported-report-set />
|
||||
<n0:getctag xmlns:n0="http://calendarserver.org/ns/" />
|
||||
<sync-token />
|
||||
</prop>
|
||||
</propfind>
|
||||
"#
|
||||
).unwrap();
|
||||
}
|
||||
@@ -8,7 +8,6 @@ publish = false
|
||||
|
||||
[dependencies]
|
||||
rustical_xml.workspace = true
|
||||
actix-web = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
futures-util = { workspace = true }
|
||||
quick-xml = { workspace = true }
|
||||
@@ -18,9 +17,8 @@ itertools = { workspace = true }
|
||||
log = { workspace = true }
|
||||
derive_more = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-actix-web = { workspace = true }
|
||||
reqwest.workspace = true
|
||||
tokio.workspace = true
|
||||
rustical_dav.workspace = true
|
||||
rustical_store.workspace = true
|
||||
web-push = { version = "0.11", default-features = false }
|
||||
http.workspace = true
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::{ContentUpdate, PropertyUpdate, SupportedTriggers, Transports, Trigger};
|
||||
use rustical_dav::header::Depth;
|
||||
use rustical_xml::{EnumUnitVariants, EnumVariants, XmlDeserialize, XmlSerialize};
|
||||
use rustical_xml::{EnumVariants, PropName, XmlDeserialize, XmlSerialize};
|
||||
|
||||
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumUnitVariants, EnumVariants)]
|
||||
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, PropName, EnumVariants)]
|
||||
#[xml(unit_variants_ident = "DavPushExtensionPropName")]
|
||||
pub enum DavPushExtensionProp {
|
||||
// WebDav Push
|
||||
|
||||
@@ -2,6 +2,30 @@ mod extension;
|
||||
pub mod notifier;
|
||||
mod prop;
|
||||
pub mod register;
|
||||
|
||||
use derive_more::Constructor;
|
||||
pub use extension::*;
|
||||
pub use prop::*;
|
||||
use rustical_store::{CollectionOperation, SubscriptionStore};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::mpsc::Receiver;
|
||||
use tracing::error;
|
||||
|
||||
#[derive(Debug, Constructor)]
|
||||
pub struct DavPushController<S: SubscriptionStore> {
|
||||
allowed_push_servers: Option<Vec<String>>,
|
||||
sub_store: Arc<S>,
|
||||
}
|
||||
|
||||
impl<S: SubscriptionStore> DavPushController<S> {
|
||||
pub async fn notifier(&self, mut recv: Receiver<CollectionOperation>) {
|
||||
while let Some(message) = recv.recv().await {
|
||||
let subscribers = match self.sub_store.get_subscriptions(&message.topic).await {
|
||||
Ok(subs) => subs,
|
||||
Err(err) => {
|
||||
error!("{err}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use actix_web::http::StatusCode;
|
||||
use http::StatusCode;
|
||||
use reqwest::{
|
||||
Method, Request,
|
||||
header::{self, HeaderName, HeaderValue},
|
||||
@@ -9,7 +9,7 @@ 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};
|
||||
// use web_push::{SubscriptionInfo, WebPushMessage, WebPushMessageBuilder};
|
||||
|
||||
#[derive(XmlSerialize, Debug)]
|
||||
struct PushMessageProp {
|
||||
@@ -30,39 +30,39 @@ struct PushMessage {
|
||||
propstat: PropstatElement<PushMessageProp>,
|
||||
}
|
||||
|
||||
pub fn build_request(message: WebPushMessage) -> Request {
|
||||
// A little janky :)
|
||||
let url = reqwest::Url::from_str(&message.endpoint.to_string()).unwrap();
|
||||
let mut builder = Request::new(Method::POST, url);
|
||||
|
||||
if let Some(topic) = message.topic {
|
||||
builder
|
||||
.headers_mut()
|
||||
.insert("Topic", HeaderValue::from_str(topic.as_str()).unwrap());
|
||||
}
|
||||
|
||||
if let Some(payload) = message.payload {
|
||||
builder.headers_mut().insert(
|
||||
header::CONTENT_ENCODING,
|
||||
HeaderValue::from_static(payload.content_encoding.to_str()),
|
||||
);
|
||||
builder.headers_mut().insert(
|
||||
header::CONTENT_TYPE,
|
||||
HeaderValue::from_static("application/octet-stream"),
|
||||
);
|
||||
|
||||
for (k, v) in payload.crypto_headers.into_iter() {
|
||||
let v: &str = v.as_ref();
|
||||
builder.headers_mut().insert(
|
||||
HeaderName::from_static(k),
|
||||
HeaderValue::from_str(&v).unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
*builder.body_mut() = Some(reqwest::Body::from(payload.content));
|
||||
}
|
||||
builder
|
||||
}
|
||||
// pub 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>>,
|
||||
@@ -102,46 +102,46 @@ pub async fn push_notifier(
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
// 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
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,15 +7,17 @@ repository.workspace = true
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
tower.workspace = true
|
||||
http.workspace = true
|
||||
axum.workspace = true
|
||||
askama.workspace = true
|
||||
async-trait.workspace = true
|
||||
askama_web.workspace = true
|
||||
actix-session.workspace = true
|
||||
async-trait.workspace = true
|
||||
serde.workspace = true
|
||||
thiserror.workspace = true
|
||||
tokio.workspace = true
|
||||
actix-web.workspace = true
|
||||
rustical_store.workspace = true
|
||||
rustical_ical.workspace = true
|
||||
rust-embed.workspace = true
|
||||
futures-core.workspace = true
|
||||
hex.workspace = true
|
||||
@@ -27,3 +29,7 @@ uuid.workspace = true
|
||||
url.workspace = true
|
||||
tracing.workspace = true
|
||||
rustical_oidc.workspace = true
|
||||
axum-extra.workspace = true
|
||||
headers.workspace = true
|
||||
tower-sessions.workspace = true
|
||||
percent-encoding.workspace = true
|
||||
|
||||
13170
crates/frontend/public/assets/licenses.html
Normal file
13170
crates/frontend/public/assets/licenses.html
Normal file
File diff suppressed because it is too large
Load Diff
@@ -62,7 +62,8 @@ html {
|
||||
background-color: var(--background-color);
|
||||
}
|
||||
|
||||
button {
|
||||
button,
|
||||
.button {
|
||||
border: none;
|
||||
background: var(--primary-color);
|
||||
padding: 8px 12px;
|
||||
@@ -159,14 +160,14 @@ table {
|
||||
display: grid;
|
||||
min-height: 80px;
|
||||
grid-template-areas:
|
||||
". color-chip"
|
||||
"title color-chip"
|
||||
"description color-chip"
|
||||
"subscription-url color-chip"
|
||||
"restore color-chip"
|
||||
". color-chip";
|
||||
grid-template-rows: 12px auto auto auto 12px;
|
||||
grid-template-columns: auto 80px;
|
||||
". . color-chip"
|
||||
"title comps color-chip"
|
||||
"description . color-chip"
|
||||
"subscription-url . color-chip"
|
||||
"actions . color-chip"
|
||||
". . color-chip";
|
||||
grid-template-rows: 12px auto auto auto auto 12px;
|
||||
grid-template-columns: min-content auto 80px;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
padding-left: 12px;
|
||||
@@ -180,14 +181,35 @@ table {
|
||||
.title {
|
||||
font-weight: bold;
|
||||
grid-area: title;
|
||||
margin-right: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
span {
|
||||
margin: 8px initial;
|
||||
}
|
||||
|
||||
.comps {
|
||||
grid-area: comps;
|
||||
|
||||
span {
|
||||
margin: 0 2px;
|
||||
background: var(--primary-color);
|
||||
color: var(--text-on-primary-color);
|
||||
font-size: .8em;
|
||||
padding: 3px 8px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
grid-area: description;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.subscription-url {
|
||||
grid-area: subscription-url;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.color-chip {
|
||||
@@ -195,8 +217,9 @@ table {
|
||||
grid-area: color-chip;
|
||||
}
|
||||
|
||||
.restore-form {
|
||||
grid-area: restore;
|
||||
.actions {
|
||||
grid-area: actions;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
|
||||
@@ -22,4 +22,7 @@
|
||||
{% block content %}<p>Placeholder</p>{% endblock %}
|
||||
</div>
|
||||
</body>
|
||||
<footer>
|
||||
<a href="/frontend/assets/licenses.html" target="_blank">Open Source Licenses</a>
|
||||
</footer>
|
||||
</html>
|
||||
|
||||
@@ -10,4 +10,12 @@
|
||||
|
||||
<pre>{{ addressbook|json }}</pre>
|
||||
|
||||
<h2>Delete</h2>
|
||||
|
||||
<section>
|
||||
<form method="POST" action="/frontend/user/{{addressbook.principal}}/addressbook/{{addressbook.id}}/delete">
|
||||
<button type="submit">Move to trash</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -29,6 +29,14 @@
|
||||
<textarea rows="16" readonly>{{ timezone }}</textarea>
|
||||
{% endif %}
|
||||
|
||||
|
||||
<pre>{{ calendar|json }}</pre>
|
||||
|
||||
<h2>Delete</h2>
|
||||
|
||||
<section>
|
||||
<form method="POST" action="/frontend/user/{{calendar.principal}}/calendar/{{calendar.id}}/delete">
|
||||
<button type="submit">Move to trash</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
{%endblock %}
|
||||
|
||||
@@ -39,4 +39,5 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -54,6 +54,9 @@
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% if let Some(hostname) = davx5_hostname %}
|
||||
<a href="intent://{{ hostname | urlencode }}#Intent;action=android.intent.action.VIEW;component=at.bitfire.davdroid.ui.setup.LoginActivity;scheme=davx5;package=at.bitfire.davdroid;S.loginFlow=1;end">Configure in DAVx5</a>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<section>
|
||||
@@ -64,12 +67,22 @@
|
||||
<li class="collection-list-item" style="--color: {{ color }}">
|
||||
<a href="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id }}">
|
||||
<span class="title">{{ calendar.displayname.to_owned().unwrap_or(calendar.id.to_owned()) }}</span>
|
||||
<div class="comps">
|
||||
{% for comp in calendar.components %}
|
||||
<span>{{ comp }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<span class="description">
|
||||
{% if let Some(description) = calendar.description %}{{ description }}{% endif %}
|
||||
</span>
|
||||
{% if let Some(subscription_url) = calendar.subscription_url %}
|
||||
<span class="subscription-url">{{ subscription_url }}</span>
|
||||
{% endif %}
|
||||
<div class="actions">
|
||||
<form action="/caldav/principal/{{ calendar.principal }}/calendar/{{ calendar.id }}" target="_blank" method="GET">
|
||||
<button type="submit">Download</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="color-chip"></div>
|
||||
</a>
|
||||
</li>
|
||||
@@ -85,18 +98,71 @@
|
||||
<li class="collection-list-item" style="--color: {{ color }}">
|
||||
<a href="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id}}">
|
||||
<span class="title">{{ calendar.displayname.to_owned().unwrap_or(calendar.id.to_owned()) }}</span>
|
||||
<div class="comps">
|
||||
{% for comp in calendar.components %}
|
||||
<span>{{ comp }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<span class="description">
|
||||
{% if let Some(description) = calendar.description %}{{ description }}{% endif %}
|
||||
</span>
|
||||
<form action="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id}}/restore" method="POST" class="restore-form">
|
||||
<button type="submit">Restore</button>
|
||||
</form>
|
||||
<div class="actions">
|
||||
<form action="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id}}/restore" method="POST" class="restore-form">
|
||||
<button type="submit">Restore</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="color-chip"></div>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
<section>
|
||||
<h3>Create calendar</h3>
|
||||
<form action="/frontend/user/{{ user.id }}/calendar" method="POST">
|
||||
<label>
|
||||
href
|
||||
<input type="text" name="id" />
|
||||
</label>
|
||||
<br>
|
||||
<label>
|
||||
Displayname
|
||||
<input type="text" name="displayname" />
|
||||
</label>
|
||||
<br>
|
||||
<label>
|
||||
Description
|
||||
<input type="text" name="description" />
|
||||
</label>
|
||||
<br>
|
||||
<label>
|
||||
Color
|
||||
<input type="color" name="color" />
|
||||
</label>
|
||||
<br>
|
||||
<label>
|
||||
Subscription URL
|
||||
<input type="text" name="subscription_url" />
|
||||
</label>
|
||||
<br>
|
||||
<label>
|
||||
Support VEVENT
|
||||
<input type="checkbox" name="comp_event" checked />
|
||||
</label>
|
||||
<label>
|
||||
Support VTODO
|
||||
<input type="checkbox" name="comp_todo" checked />
|
||||
</label>
|
||||
<label>
|
||||
Support VJOURNAL
|
||||
<input type="checkbox" name="comp_journal" />
|
||||
</label>
|
||||
<br>
|
||||
<button type="submit">Create</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
</section>
|
||||
|
||||
<section>
|
||||
@@ -109,6 +175,11 @@
|
||||
<span class="description">
|
||||
{% if let Some(description) = addressbook.description %}{{ description }}{% endif %}
|
||||
</span>
|
||||
<div class="actions">
|
||||
<form action="/carddav/principal/{{ addressbook.principal }}/{{ addressbook.id }}" target="_blank" method="GET">
|
||||
<button type="submit">Download</button>
|
||||
</form>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
@@ -125,14 +196,39 @@
|
||||
<span class="description">
|
||||
{% if let Some(description) = addressbook.description %}{{ description }}{% endif %}
|
||||
</span>
|
||||
<form action="/frontend/user/{{ addressbook.principal }}/addressbook/{{ addressbook.id}}/restore" method="POST" class="restore-form">
|
||||
<button type="submit">Restore</button>
|
||||
</form>
|
||||
<div class="actions">
|
||||
<form action="/frontend/user/{{ addressbook.principal }}/addressbook/{{ addressbook.id}}/restore" method="POST" class="restore-form">
|
||||
<button type="submit">Restore</button>
|
||||
</form>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
<section>
|
||||
<h3>Create addressbook</h3>
|
||||
<form action="/frontend/user/{{ user.id }}/addressbook" method="POST">
|
||||
<label>
|
||||
href
|
||||
<input type="text" name="id" />
|
||||
</label>
|
||||
<br>
|
||||
<label>
|
||||
Displayname
|
||||
<input type="text" name="displayname" />
|
||||
</label>
|
||||
<br>
|
||||
<label>
|
||||
Description
|
||||
<input type="text" name="description" />
|
||||
</label>
|
||||
<br>
|
||||
<button type="submit">Create</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@@ -1,97 +1,72 @@
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use actix_web::{
|
||||
body::BoxBody,
|
||||
dev::{
|
||||
HttpServiceFactory, ResourceDef, Service, ServiceFactory, ServiceRequest, ServiceResponse,
|
||||
},
|
||||
http::{header, Method},
|
||||
HttpResponse,
|
||||
use axum::{
|
||||
RequestExt,
|
||||
body::Body,
|
||||
extract::{Path, Request},
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use futures_core::future::LocalBoxFuture;
|
||||
use futures_core::future::BoxFuture;
|
||||
use headers::{ContentType, ETag, HeaderMapExt};
|
||||
use http::{Method, StatusCode};
|
||||
use rust_embed::RustEmbed;
|
||||
use std::{convert::Infallible, marker::PhantomData, str::FromStr};
|
||||
use tower::Service;
|
||||
|
||||
#[derive(RustEmbed)]
|
||||
#[derive(Clone, RustEmbed)]
|
||||
#[folder = "public/assets"]
|
||||
pub struct Assets;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct EmbedService<E>
|
||||
where
|
||||
E: 'static + RustEmbed,
|
||||
{
|
||||
_embed: PhantomData<E>,
|
||||
prefix: String,
|
||||
}
|
||||
|
||||
impl<E> EmbedService<E>
|
||||
where
|
||||
E: 'static + RustEmbed,
|
||||
{
|
||||
pub fn new(prefix: String) -> Self {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
prefix,
|
||||
_embed: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> HttpServiceFactory for EmbedService<E>
|
||||
impl<E> Service<Request> for EmbedService<E>
|
||||
where
|
||||
E: 'static + RustEmbed,
|
||||
{
|
||||
fn register(self, config: &mut actix_web::dev::AppService) {
|
||||
let resource_def = if config.is_root() {
|
||||
ResourceDef::root_prefix(&self.prefix)
|
||||
} else {
|
||||
ResourceDef::prefix(&self.prefix)
|
||||
};
|
||||
config.register_service(resource_def, None, self, None);
|
||||
type Response = Response;
|
||||
type Error = Infallible;
|
||||
type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;
|
||||
|
||||
#[inline]
|
||||
fn poll_ready(
|
||||
&mut self,
|
||||
_cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<Result<(), Self::Error>> {
|
||||
Ok(()).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> ServiceFactory<ServiceRequest> for EmbedService<E>
|
||||
where
|
||||
E: 'static + RustEmbed,
|
||||
{
|
||||
type Response = ServiceResponse;
|
||||
type Error = actix_web::Error;
|
||||
type Config = ();
|
||||
type Service = EmbedService<E>;
|
||||
type InitError = ();
|
||||
type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>;
|
||||
|
||||
fn new_service(&self, _: ()) -> Self::Future {
|
||||
let prefix = self.prefix.clone();
|
||||
Box::pin(async move {
|
||||
Ok(Self {
|
||||
prefix,
|
||||
_embed: PhantomData,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> Service<ServiceRequest> for EmbedService<E>
|
||||
where
|
||||
E: 'static + RustEmbed,
|
||||
{
|
||||
type Response = ServiceResponse<BoxBody>;
|
||||
type Error = actix_web::Error;
|
||||
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
|
||||
|
||||
actix_web::dev::always_ready!();
|
||||
|
||||
fn call(&self, req: ServiceRequest) -> Self::Future {
|
||||
#[inline]
|
||||
fn call(&mut self, mut req: Request) -> Self::Future {
|
||||
Box::pin(async move {
|
||||
if req.method() != Method::GET && req.method() != Method::HEAD {
|
||||
return Ok(req.into_response(HttpResponse::MethodNotAllowed()));
|
||||
return Ok(StatusCode::METHOD_NOT_ALLOWED.into_response());
|
||||
}
|
||||
let path = req.match_info().unprocessed().trim_start_matches('/');
|
||||
let path: String = if let Ok(Path(path)) = req.extract_parts().await.unwrap() {
|
||||
path
|
||||
} else {
|
||||
return Ok(StatusCode::NOT_FOUND.into_response());
|
||||
};
|
||||
|
||||
match E::get(path) {
|
||||
match E::get(&path) {
|
||||
Some(file) => {
|
||||
let data = file.data;
|
||||
let hash = hex::encode(file.metadata.sha256_hash());
|
||||
let etag = format!("\"{hash}\"");
|
||||
let mime = mime_guess::from_path(path).first_or_octet_stream();
|
||||
|
||||
let body = if req.method() == Method::HEAD {
|
||||
@@ -99,14 +74,13 @@ where
|
||||
} else {
|
||||
data
|
||||
};
|
||||
Ok(req.into_response(
|
||||
HttpResponse::Ok()
|
||||
.content_type(mime)
|
||||
.insert_header((header::ETAG, hash))
|
||||
.body(body),
|
||||
))
|
||||
let mut res = Response::builder().status(StatusCode::OK);
|
||||
let hdrs = res.headers_mut().unwrap();
|
||||
hdrs.typed_insert(ContentType::from(mime));
|
||||
hdrs.typed_insert(ETag::from_str(&etag).unwrap());
|
||||
Ok(res.body(Body::from(body)).unwrap())
|
||||
}
|
||||
None => Ok(req.into_response(HttpResponse::NotFound())),
|
||||
None => Ok(StatusCode::NOT_FOUND.into_response()),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,9 +7,6 @@ fn default_true() -> bool {
|
||||
#[derive(Deserialize, Serialize, Clone)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct FrontendConfig {
|
||||
#[serde(serialize_with = "hex::serde::serialize")]
|
||||
#[serde(deserialize_with = "hex::serde::deserialize")]
|
||||
pub secret_key: [u8; 64],
|
||||
#[serde(default = "default_true")]
|
||||
pub enabled: bool,
|
||||
#[serde(default = "default_true")]
|
||||
|
||||
@@ -1,360 +1,165 @@
|
||||
use actix_session::{
|
||||
SessionMiddleware,
|
||||
config::CookieContentSecurity,
|
||||
storage::{CookieSessionStore, SessionStore},
|
||||
use axum::{
|
||||
Extension, RequestExt, Router,
|
||||
body::Body,
|
||||
extract::{OriginalUri, Request},
|
||||
middleware::{self, Next},
|
||||
response::{Redirect, Response},
|
||||
routing::{get, post},
|
||||
};
|
||||
use actix_web::{
|
||||
HttpRequest, HttpResponse, Responder,
|
||||
cookie::{Key, SameSite},
|
||||
dev::ServiceResponse,
|
||||
http::{Method, StatusCode, header},
|
||||
middleware::{ErrorHandlerResponse, ErrorHandlers},
|
||||
web::{self, Data, Form, Path, Redirect},
|
||||
};
|
||||
use askama::Template;
|
||||
use askama_web::WebTemplate;
|
||||
use assets::{Assets, EmbedService};
|
||||
use async_trait::async_trait;
|
||||
use rand::{Rng, distributions::Alphanumeric};
|
||||
use routes::{
|
||||
addressbook::{route_addressbook, route_addressbook_restore},
|
||||
calendar::{route_calendar, route_calendar_restore},
|
||||
login::{route_get_login, route_post_login, route_post_logout},
|
||||
};
|
||||
use rustical_oidc::{OidcConfig, OidcServiceConfig, UserStore, configure_oidc};
|
||||
use headers::{ContentType, HeaderMapExt};
|
||||
use http::{Method, StatusCode};
|
||||
use rustical_oidc::{OidcConfig, OidcServiceConfig, route_get_oidc_callback, route_post_oidc};
|
||||
use rustical_store::{
|
||||
Addressbook, AddressbookStore, Calendar, CalendarStore,
|
||||
auth::{AuthenticationMiddleware, AuthenticationProvider, User, user::AppToken},
|
||||
AddressbookStore, CalendarStore,
|
||||
auth::{AuthenticationProvider, middleware::AuthenticationLayer},
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
use url::Url;
|
||||
|
||||
mod assets;
|
||||
mod config;
|
||||
pub mod nextcloud_login;
|
||||
mod oidc_user_store;
|
||||
mod routes;
|
||||
|
||||
pub const ROUTE_NAME_HOME: &str = "frontend_home";
|
||||
pub const ROUTE_USER_NAMED: &str = "frontend_user_named";
|
||||
|
||||
pub use config::FrontendConfig;
|
||||
use oidc_user_store::OidcUserStore;
|
||||
|
||||
pub fn generate_app_token() -> String {
|
||||
rand::thread_rng()
|
||||
.sample_iter(Alphanumeric)
|
||||
.map(char::from)
|
||||
.take(64)
|
||||
.collect()
|
||||
}
|
||||
use crate::{
|
||||
assets::{Assets, EmbedService},
|
||||
routes::{
|
||||
addressbook::{
|
||||
route_addressbook, route_addressbook_restore, route_create_addressbook,
|
||||
route_delete_addressbook,
|
||||
},
|
||||
app_token::{route_delete_app_token, route_post_app_token},
|
||||
calendar::{
|
||||
route_calendar, route_calendar_restore, route_create_calendar, route_delete_calendar,
|
||||
},
|
||||
login::{route_get_login, route_post_login, route_post_logout},
|
||||
user::{route_get_home, route_root, route_user_named},
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Template, WebTemplate)]
|
||||
#[template(path = "pages/user.html")]
|
||||
struct UserPage {
|
||||
pub user: User,
|
||||
pub app_tokens: Vec<AppToken>,
|
||||
pub calendars: Vec<Calendar>,
|
||||
pub deleted_calendars: Vec<Calendar>,
|
||||
pub addressbooks: Vec<Addressbook>,
|
||||
pub deleted_addressbooks: Vec<Addressbook>,
|
||||
pub is_apple: bool,
|
||||
}
|
||||
|
||||
async fn route_user_named<CS: CalendarStore, AS: AddressbookStore, AP: AuthenticationProvider>(
|
||||
path: Path<String>,
|
||||
cal_store: Data<CS>,
|
||||
addr_store: Data<AS>,
|
||||
auth_provider: Data<AP>,
|
||||
user: User,
|
||||
req: HttpRequest,
|
||||
) -> impl Responder {
|
||||
let user_id = path.into_inner();
|
||||
if user_id != user.id {
|
||||
return actix_web::HttpResponse::Unauthorized().body("Unauthorized");
|
||||
}
|
||||
|
||||
let mut calendars = vec![];
|
||||
for group in user.memberships() {
|
||||
calendars.extend(cal_store.get_calendars(group).await.unwrap());
|
||||
}
|
||||
|
||||
let mut deleted_calendars = vec![];
|
||||
for group in user.memberships() {
|
||||
deleted_calendars.extend(cal_store.get_deleted_calendars(group).await.unwrap());
|
||||
}
|
||||
|
||||
let mut addressbooks = vec![];
|
||||
for group in user.memberships() {
|
||||
addressbooks.extend(addr_store.get_addressbooks(group).await.unwrap());
|
||||
}
|
||||
|
||||
let mut deleted_addressbooks = vec![];
|
||||
for group in user.memberships() {
|
||||
deleted_addressbooks.extend(addr_store.get_deleted_addressbooks(group).await.unwrap());
|
||||
}
|
||||
|
||||
let is_apple = req
|
||||
.headers()
|
||||
.get(header::USER_AGENT)
|
||||
.and_then(|user_agent| user_agent.to_str().ok())
|
||||
.map(|ua| ua.contains("Apple") || ua.contains("Mac OS"))
|
||||
.unwrap_or_default();
|
||||
|
||||
UserPage {
|
||||
app_tokens: auth_provider.get_app_tokens(&user.id).await.unwrap(),
|
||||
calendars,
|
||||
deleted_calendars,
|
||||
addressbooks,
|
||||
deleted_addressbooks,
|
||||
user,
|
||||
is_apple,
|
||||
}
|
||||
.respond_to(&req)
|
||||
}
|
||||
|
||||
async fn route_get_home(user: User, req: HttpRequest) -> Redirect {
|
||||
Redirect::to(
|
||||
req.url_for(ROUTE_USER_NAMED, &[user.id])
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
)
|
||||
.see_other()
|
||||
}
|
||||
|
||||
async fn route_root(user: Option<User>, req: HttpRequest) -> impl Responder {
|
||||
let redirect_url = match user {
|
||||
Some(_) => req.url_for_static(ROUTE_NAME_HOME).unwrap(),
|
||||
None => req
|
||||
.resource_map()
|
||||
.url_for::<[_; 0], String>(&req, "frontend_login", [])
|
||||
.unwrap(),
|
||||
};
|
||||
web::Redirect::to(redirect_url.to_string()).permanent()
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "apple_configuration/template.xml")]
|
||||
pub struct AppleConfig {
|
||||
token_name: String,
|
||||
account_description: String,
|
||||
hostname: String,
|
||||
caldav_principal_url: String,
|
||||
carddav_principal_url: String,
|
||||
user: String,
|
||||
token: String,
|
||||
caldav_profile_uuid: Uuid,
|
||||
carddav_profile_uuid: Uuid,
|
||||
plist_uuid: Uuid,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub(crate) struct PostAppTokenForm {
|
||||
name: String,
|
||||
#[serde(default)]
|
||||
apple: bool,
|
||||
}
|
||||
|
||||
async fn route_post_app_token<AP: AuthenticationProvider>(
|
||||
user: User,
|
||||
auth_provider: Data<AP>,
|
||||
path: Path<String>,
|
||||
Form(PostAppTokenForm { apple, name }): Form<PostAppTokenForm>,
|
||||
req: HttpRequest,
|
||||
) -> Result<HttpResponse, rustical_store::Error> {
|
||||
assert!(!name.is_empty());
|
||||
assert_eq!(path.into_inner(), user.id);
|
||||
let token = generate_app_token();
|
||||
auth_provider
|
||||
.add_app_token(&user.id, name.to_owned(), token.clone())
|
||||
.await?;
|
||||
if apple {
|
||||
let hostname = req.full_url().host_str().unwrap().to_owned();
|
||||
let profile = AppleConfig {
|
||||
token_name: name,
|
||||
account_description: format!("{}@{}", &user.id, &hostname),
|
||||
hostname,
|
||||
caldav_principal_url: req
|
||||
.url_for("caldav_principal", [&user.id])
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
carddav_principal_url: req
|
||||
.url_for("carddav_principal", [&user.id])
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
user: user.id.to_owned(),
|
||||
token,
|
||||
caldav_profile_uuid: Uuid::new_v4(),
|
||||
carddav_profile_uuid: Uuid::new_v4(),
|
||||
plist_uuid: Uuid::new_v4(),
|
||||
}
|
||||
.render()
|
||||
.unwrap();
|
||||
Ok(HttpResponse::Ok()
|
||||
.insert_header(header::ContentDisposition::attachment(format!(
|
||||
"rustical-{}.mobileconfig",
|
||||
user.id
|
||||
)))
|
||||
.insert_header((
|
||||
header::CONTENT_TYPE,
|
||||
"application/x-apple-aspen-config; charset=utf-8",
|
||||
))
|
||||
.body(profile))
|
||||
} else {
|
||||
Ok(HttpResponse::Ok().body(token))
|
||||
}
|
||||
}
|
||||
|
||||
async fn route_delete_app_token<AP: AuthenticationProvider>(
|
||||
user: User,
|
||||
auth_provider: Data<AP>,
|
||||
path: Path<(String, String)>,
|
||||
) -> Result<Redirect, rustical_store::Error> {
|
||||
let (path_user, token_id) = path.into_inner();
|
||||
assert_eq!(path_user, user.id);
|
||||
auth_provider.remove_app_token(&user.id, &token_id).await?;
|
||||
Ok(Redirect::to("/frontend/user").see_other())
|
||||
}
|
||||
|
||||
pub(crate) fn unauthorized_handler<B>(
|
||||
res: ServiceResponse<B>,
|
||||
) -> actix_web::Result<ErrorHandlerResponse<B>> {
|
||||
let (req, _) = res.into_parts();
|
||||
let redirect_uri = req.uri().to_string();
|
||||
let mut login_url = req.url_for_static("frontend_login").unwrap();
|
||||
login_url
|
||||
.query_pairs_mut()
|
||||
.append_pair("redirect_uri", &redirect_uri);
|
||||
let login_url = login_url.to_string();
|
||||
|
||||
let response = HttpResponse::Unauthorized().body(format!(
|
||||
r#"<!Doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="refresh" content="1; url={login_url}" />
|
||||
</head>
|
||||
<body>
|
||||
Unauthorized, redirecting to <a href="{login_url}">login page</a>
|
||||
</body>
|
||||
<html>
|
||||
"#
|
||||
));
|
||||
|
||||
let res = ServiceResponse::new(req, response)
|
||||
.map_into_boxed_body()
|
||||
.map_into_right_body();
|
||||
|
||||
Ok(ErrorHandlerResponse::Response(res))
|
||||
}
|
||||
|
||||
pub fn session_middleware(frontend_secret: [u8; 64]) -> SessionMiddleware<impl SessionStore> {
|
||||
SessionMiddleware::builder(CookieSessionStore::default(), Key::from(&frontend_secret))
|
||||
.cookie_secure(true)
|
||||
.cookie_same_site(SameSite::Strict)
|
||||
.cookie_content_security(CookieContentSecurity::Private)
|
||||
.build()
|
||||
}
|
||||
|
||||
pub fn configure_frontend<AP: AuthenticationProvider, CS: CalendarStore, AS: AddressbookStore>(
|
||||
cfg: &mut web::ServiceConfig,
|
||||
pub fn frontend_router<AP: AuthenticationProvider, CS: CalendarStore, AS: AddressbookStore>(
|
||||
prefix: &'static str,
|
||||
auth_provider: Arc<AP>,
|
||||
cal_store: Arc<CS>,
|
||||
addr_store: Arc<AS>,
|
||||
frontend_config: FrontendConfig,
|
||||
oidc_config: Option<OidcConfig>,
|
||||
) {
|
||||
let mut scope = web::scope("")
|
||||
.wrap(ErrorHandlers::new().handler(StatusCode::UNAUTHORIZED, unauthorized_handler))
|
||||
.wrap(AuthenticationMiddleware::new(auth_provider.clone()))
|
||||
.wrap(session_middleware(frontend_config.secret_key))
|
||||
.app_data(Data::from(auth_provider.clone()))
|
||||
.app_data(Data::from(cal_store.clone()))
|
||||
.app_data(Data::from(addr_store.clone()))
|
||||
.app_data(Data::new(frontend_config.clone()))
|
||||
.app_data(Data::new(oidc_config.clone()))
|
||||
.service(EmbedService::<Assets>::new("/assets".to_owned()))
|
||||
.service(web::resource("").route(web::method(Method::GET).to(route_root)))
|
||||
.service(
|
||||
web::resource("/user")
|
||||
.get(route_get_home)
|
||||
.name(ROUTE_NAME_HOME),
|
||||
)
|
||||
.service(
|
||||
web::resource("/user/{user}")
|
||||
.get(route_user_named::<CS, AS, AP>)
|
||||
.name(ROUTE_USER_NAMED),
|
||||
)
|
||||
) -> Router {
|
||||
let mut router = Router::new();
|
||||
router = router
|
||||
.route("/", get(route_root))
|
||||
.route("/user", get(route_get_home))
|
||||
.route("/user/{user}", get(route_user_named::<CS, AS, AP>))
|
||||
// App token management
|
||||
.service(web::resource("/user/{user}/app_token").post(route_post_app_token::<AP>))
|
||||
.service(
|
||||
.route("/user/{user}/app_token", post(route_post_app_token::<AP>))
|
||||
.route(
|
||||
// POST because HTML5 forms don't support DELETE method
|
||||
web::resource("/user/{user}/app_token/{id}/delete").post(route_delete_app_token::<AP>),
|
||||
"/user/{user}/app_token/{id}/delete",
|
||||
post(route_delete_app_token::<AP>),
|
||||
)
|
||||
// Calendar
|
||||
.service(web::resource("/user/{user}/calendar/{calendar}").get(route_calendar::<CS>))
|
||||
.service(
|
||||
web::resource("/user/{user}/calendar/{calendar}/restore")
|
||||
.post(route_calendar_restore::<CS>),
|
||||
.route("/user/{user}/calendar", post(route_create_calendar::<CS>))
|
||||
.route(
|
||||
"/user/{user}/calendar/{calendar}",
|
||||
get(route_calendar::<CS>),
|
||||
)
|
||||
.route(
|
||||
"/user/{user}/calendar/{calendar}/delete",
|
||||
post(route_delete_calendar::<CS>),
|
||||
)
|
||||
.route(
|
||||
"/user/{user}/calendar/{calendar}/restore",
|
||||
post(route_calendar_restore::<CS>),
|
||||
)
|
||||
// Addressbook
|
||||
.service(
|
||||
web::resource("/user/{user}/addressbook/{addressbook}").get(route_addressbook::<AS>),
|
||||
.route(
|
||||
"/user/{user}/addressbook",
|
||||
post(route_create_addressbook::<AS>),
|
||||
)
|
||||
.service(
|
||||
web::resource("/user/{user}/addressbook/{addressbook}/restore")
|
||||
.post(route_addressbook_restore::<AS>),
|
||||
.route(
|
||||
"/user/{user}/addressbook/{addressbook}",
|
||||
get(route_addressbook::<AS>),
|
||||
)
|
||||
// Login
|
||||
.service(
|
||||
web::resource("/login")
|
||||
.name("frontend_login")
|
||||
.get(route_get_login)
|
||||
.post(route_post_login::<AP>),
|
||||
.route(
|
||||
"/user/{user}/addressbook/{addressbook}/delete",
|
||||
post(route_delete_addressbook::<AS>),
|
||||
)
|
||||
.service(
|
||||
web::resource("/logout")
|
||||
.name("frontend_logout")
|
||||
.post(route_post_logout),
|
||||
);
|
||||
.route(
|
||||
"/user/{user}/addressbook/{addressbook}/restore",
|
||||
post(route_addressbook_restore::<AS>),
|
||||
)
|
||||
.route("/login", get(route_get_login).post(route_post_login::<AP>))
|
||||
.route("/logout", post(route_post_logout))
|
||||
.route_service("/assets/{*file}", EmbedService::<Assets>::new());
|
||||
|
||||
if let Some(oidc_config) = oidc_config {
|
||||
scope = scope.service(web::scope("/login/oidc").configure(|cfg| {
|
||||
configure_oidc(
|
||||
cfg,
|
||||
oidc_config,
|
||||
OidcServiceConfig {
|
||||
default_redirect_route_name: ROUTE_NAME_HOME,
|
||||
session_key_user_id: "user",
|
||||
},
|
||||
Arc::new(OidcUserStore(auth_provider.clone())),
|
||||
if let Some(oidc_config) = oidc_config.clone() {
|
||||
router = router
|
||||
.route("/login/oidc", post(route_post_oidc))
|
||||
.route(
|
||||
"/login/oidc/callback",
|
||||
get(route_get_oidc_callback::<OidcUserStore<AP>>),
|
||||
)
|
||||
}));
|
||||
.layer(Extension(OidcUserStore(auth_provider.clone())))
|
||||
.layer(Extension(OidcServiceConfig {
|
||||
default_redirect_path: "/frontend/user",
|
||||
session_key_user_id: "user",
|
||||
}))
|
||||
.layer(Extension(oidc_config));
|
||||
}
|
||||
|
||||
cfg.service(scope);
|
||||
router = router
|
||||
.layer(AuthenticationLayer::new(auth_provider.clone()))
|
||||
.layer(Extension(auth_provider.clone()))
|
||||
.layer(Extension(cal_store.clone()))
|
||||
.layer(Extension(addr_store.clone()))
|
||||
.layer(Extension(frontend_config.clone()))
|
||||
.layer(Extension(oidc_config.clone()))
|
||||
.layer(middleware::from_fn(unauthorized_handler));
|
||||
|
||||
Router::new()
|
||||
.nest(prefix, router)
|
||||
.route("/", get(async || Redirect::to(prefix)))
|
||||
}
|
||||
|
||||
struct OidcUserStore<AP: AuthenticationProvider>(Arc<AP>);
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl<AP: AuthenticationProvider> UserStore for OidcUserStore<AP> {
|
||||
type Error = rustical_store::Error;
|
||||
|
||||
async fn user_exists(&self, id: &str) -> Result<bool, Self::Error> {
|
||||
Ok(self.0.get_principal(id).await?.is_some())
|
||||
}
|
||||
|
||||
async fn insert_user(&self, id: &str) -> Result<(), Self::Error> {
|
||||
self.0
|
||||
.insert_principal(
|
||||
User {
|
||||
id: id.to_owned(),
|
||||
displayname: None,
|
||||
principal_type: Default::default(),
|
||||
password: None,
|
||||
memberships: vec![],
|
||||
},
|
||||
false,
|
||||
)
|
||||
.await
|
||||
async fn unauthorized_handler(mut request: Request, next: Next) -> Response {
|
||||
let meth = request.method().clone();
|
||||
let OriginalUri(uri) = request.extract_parts().await.unwrap();
|
||||
let resp = next.run(request).await;
|
||||
if resp.status() == StatusCode::UNAUTHORIZED {
|
||||
// This is a dumb hack since parsed Urls cannot be relative
|
||||
let mut login_url: Url = "http://github.com/frontend/login".parse().unwrap();
|
||||
if meth == Method::GET {
|
||||
login_url
|
||||
.query_pairs_mut()
|
||||
.append_pair("redirect_uri", uri.path());
|
||||
}
|
||||
let path = login_url.path();
|
||||
let query = login_url
|
||||
.query()
|
||||
.map(|query| format!("?{query}"))
|
||||
.unwrap_or_default();
|
||||
let login_url = format!("{path}{query}");
|
||||
let mut resp = Response::builder().status(StatusCode::UNAUTHORIZED);
|
||||
let hdrs = resp.headers_mut().unwrap();
|
||||
hdrs.typed_insert(ContentType::html());
|
||||
return resp
|
||||
.body(Body::new(format!(
|
||||
r#"<!Doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="refresh" content="1; url={login_url}" />
|
||||
</head>
|
||||
<body>
|
||||
Unauthorized, redirecting to <a href="{login_url}">login page</a>
|
||||
</body>
|
||||
<html>
|
||||
"#,
|
||||
)))
|
||||
.unwrap();
|
||||
}
|
||||
resp
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user