Checkpoint: Migration to axum

This commit is contained in:
Lennart
2025-06-08 14:10:12 +02:00
parent 790c657b08
commit 95889e3df1
60 changed files with 1476 additions and 2205 deletions

View File

@@ -1,13 +1,14 @@
use crate::Error;
use crate::calendar::prop::SupportedCalendarComponentSet;
use actix_web::HttpResponse;
use actix_web::web::{Data, Path};
use crate::calendar::resource::CalendarResourceService;
use axum::extract::{Path, State};
use axum::response::{IntoResponse, Response};
use http::StatusCode;
use rustical_ical::CalendarObjectType;
use rustical_store::auth::User;
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,15 +49,13 @@ 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,
#[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>>,
body: String,
) -> Result<Response, Error> {
if !user.is_principal(&principal) {
return Err(Error::Unauthorized);
}
@@ -87,12 +86,10 @@ pub async fn route_mkcalendar<C: CalendarStore>(
]),
};
match store.insert_calendar(calendar).await {
match cal_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("")),
Ok(()) => Ok(StatusCode::CREATED.into_response()),
Err(err) => {
dbg!(err.to_string());
Err(err.into())

View File

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

View File

@@ -1,8 +1,7 @@
use crate::Error;
use crate::calendar::resource::{CalendarResource, CalendarResourceService};
use actix_web::http::header;
use actix_web::web::{Data, Path};
use actix_web::{HttpRequest, HttpResponse};
use axum::extract::{Path, State};
use axum::response::Response;
use rustical_dav::privileges::UserPrivilege;
use rustical_dav::resource::Resource;
use rustical_dav_push::register::PushRegister;
@@ -10,18 +9,14 @@ 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(resource_service, 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,
resource_service: Data<CalendarResourceService<C, 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);
}

View File

@@ -1,5 +1,4 @@
use crate::{Error, calendar_object::resource::CalendarObjectPropWrapperName};
use actix_web::dev::{Path, ResourceDef};
use rustical_dav::xml::PropfindType;
use rustical_ical::CalendarObject;
use rustical_store::CalendarStore;
@@ -23,23 +22,25 @@ 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) {
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))

View File

@@ -1,12 +1,14 @@
use crate::{
CalDavPrincipalUri, Error,
calendar::resource::CalendarResourceService,
calendar_object::resource::{
CalendarObjectPropWrapper, CalendarObjectPropWrapperName, CalendarObjectResource,
},
};
use actix_web::{
HttpRequest, Responder,
web::{Data, Path},
use axum::{
Extension,
extract::{OriginalUri, Path, State},
response::IntoResponse,
};
use calendar_multiget::{CalendarMultigetRequest, get_objects_calendar_multiget};
use calendar_query::{CalendarQueryRequest, get_objects_calendar_query};
@@ -19,7 +21,7 @@ use rustical_dav::{
},
};
use rustical_ical::CalendarObject;
use rustical_store::{CalendarStore, auth::User};
use rustical_store::{CalendarStore, SubscriptionStore, auth::User};
use rustical_xml::{XmlDeserialize, XmlDocument};
use sync_collection::handle_sync_collection;
use tracing::instrument;
@@ -85,16 +87,15 @@ fn objects_response(
})
}
#[instrument(skip(req, cal_store))]
pub async fn route_report_calendar<C: CalendarStore>(
path: Path<(String, String)>,
body: String,
#[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,
puri: Data<CalDavPrincipalUri>,
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);
}
@@ -107,20 +108,12 @@ pub async fn route_report_calendar<C: CalendarStore>(
let objects =
get_objects_calendar_query(cal_query, &principal, &cal_id, cal_store.as_ref())
.await?;
objects_response(
objects,
vec![],
req.path(),
&principal,
puri.as_ref(),
&user,
props,
)?
objects_response(objects, vec![], uri.path(), &principal, &puri, &user, props)?
}
ReportRequest::CalendarMultiget(cal_multiget) => {
let (objects, not_found) = get_objects_calendar_multiget(
cal_multiget,
req.path(),
uri.path(),
&principal,
&cal_id,
cal_store.as_ref(),
@@ -129,9 +122,9 @@ pub async fn route_report_calendar<C: CalendarStore>(
objects_response(
objects,
not_found,
req.path(),
uri.path(),
&principal,
puri.as_ref(),
&puri,
&user,
props,
)?
@@ -139,8 +132,8 @@ pub async fn route_report_calendar<C: CalendarStore>(
ReportRequest::SyncCollection(sync_collection) => {
handle_sync_collection(
sync_collection,
req.path(),
puri.as_ref(),
uri.path(),
&puri,
&user,
&principal,
&cal_id,

View File

@@ -1,19 +1,20 @@
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::calendar_object::resource::{CalendarObjectResource, CalendarObjectResourceService};
use crate::calendar::methods::mkcalendar::route_mkcalendar;
use crate::calendar::methods::report::route_report_calendar;
use crate::calendar_object::resource::CalendarObjectResource;
use crate::{CalDavPrincipalUri, Error};
use actix_web::http::Method;
use actix_web::web::{self};
use async_trait::async_trait;
use axum::extract::Request;
use axum::handler::Handler;
use axum::response::Response;
use chrono::{DateTime, Utc};
use derive_more::derive::{From, Into};
use futures_util::future::BoxFuture;
use rustical_dav::extensions::{
CommonPropertiesExtension, CommonPropertiesProp, SyncTokenExtension, SyncTokenExtensionProp,
};
use rustical_dav::privileges::UserPrivilegeSet;
use rustical_dav::resource::{PrincipalUri, Resource, ResourceService};
use rustical_dav::resource::{AxumMethods, PrincipalUri, Resource, ResourceService};
use rustical_dav::xml::{HrefElement, Resourcetype, ResourcetypeInner};
use rustical_dav_push::{DavPushExtension, DavPushExtensionProp};
use rustical_ical::CalDateTime;
@@ -21,8 +22,10 @@ use rustical_store::auth::User;
use rustical_store::{Calendar, CalendarStore, SubscriptionStore};
use rustical_xml::{EnumVariants, PropName};
use rustical_xml::{XmlDeserialize, XmlSerialize};
use std::convert::Infallible;
use std::str::FromStr;
use std::sync::Arc;
use tower::Service;
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, PropName)]
#[xml(unit_variants_ident = "CalendarPropName")]
@@ -313,6 +316,15 @@ pub struct CalendarResourceService<C: CalendarStore, S: SubscriptionStore> {
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 {
@@ -386,17 +398,21 @@ impl<C: CalendarStore, S: SubscriptionStore> ResourceService for CalendarResourc
.await?;
Ok(())
}
}
fn actix_scope(self) -> actix_web::Scope {
let report_method = web::method(Method::from_str("REPORT").unwrap());
let mkcalendar_method = web::method(Method::from_str("MKCALENDAR").unwrap());
web::scope("/{calendar_id}")
.service(CalendarObjectResourceService::new(self.cal_store.clone()).actix_scope())
.service(
self.actix_resource()
.route(report_method.to(route_report_calendar::<C>))
.route(mkcalendar_method.to(route_mkcalendar::<C>))
.post(route_post::<C, S>),
)
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 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))
})
}
}

View File

@@ -1,71 +1,70 @@
use super::resource::CalendarObjectPathComponents;
use crate::Error;
use crate::calendar_object::resource::CalendarObjectResourceService;
use crate::error::Precondition;
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 headers::{ContentType, ETag, HeaderMapExt, IfNoneMatch};
use http::StatusCode;
use rustical_ical::CalendarObject;
use rustical_store::CalendarStore;
use rustical_store::auth::User;
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,
if_none_match: Option<TypedHeader<IfNoneMatch>>,
body: String,
) -> Result<Response, Error> {
if !user.is_principal(&principal) {
return Ok(HttpResponse::Unauthorized().finish());
return Err(crate::Error::Unauthorized);
}
let overwrite =
Some(&HeaderValue::from_static("*")) != req.headers().get(header::IF_NONE_MATCH);
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,
@@ -73,9 +72,9 @@ pub async fn put_event<C: CalendarStore>(
return Err(Error::PreconditionFailed(Precondition::ValidCalendarData));
}
};
store
cal_store
.put_object(principal, calendar_id, object, overwrite)
.await?;
Ok(HttpResponse::Created().finish())
Ok(StatusCode::CREATED.into_response())
}

View File

@@ -1,22 +1,35 @@
use super::methods::{get_event, put_event};
use crate::{CalDavPrincipalUri, Error};
use actix_web::web;
// use super::methods::{get_event, put_event};
use crate::{
CalDavPrincipalUri, Error,
calendar_object::methods::{get_event, put_event},
};
use async_trait::async_trait;
use axum::{extract::Request, handler::Handler, response::Response};
use derive_more::derive::{From, Into};
use futures_util::future::BoxFuture;
use rustical_dav::{
extensions::{CommonPropertiesExtension, CommonPropertiesProp},
privileges::UserPrivilegeSet,
resource::{PrincipalUri, Resource, ResourceService},
resource::{AxumMethods, PrincipalUri, Resource, ResourceService},
xml::Resourcetype,
};
use rustical_ical::{CalendarObject, UtcDateTime};
use rustical_store::{CalendarStore, auth::User};
use rustical_xml::{EnumVariants, PropName, XmlDeserialize, XmlSerialize};
use serde::Deserialize;
use std::sync::Arc;
use serde::{Deserialize, Deserializer};
use std::{convert::Infallible, sync::Arc};
use tower::Service;
pub struct CalendarObjectResourceService<C: CalendarStore> {
cal_store: Arc<C>,
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> {
@@ -130,10 +143,23 @@ impl Resource for CalendarObjectResource {
}
}
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"))
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct CalendarObjectPathComponents {
pub principal: String,
pub calendar_id: String,
#[serde(deserialize_with = "deserialize_ics_name")]
pub object_id: String,
}
@@ -180,12 +206,19 @@ impl<C: CalendarStore> ResourceService for CalendarObjectResourceService<C> {
.await?;
Ok(())
}
}
fn actix_scope(self) -> actix_web::Scope {
web::scope("/{object_id}.ics").service(
self.actix_resource()
.get(get_event::<C>)
.put(put_event::<C>),
)
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))
})
}
}

View File

@@ -1,10 +1,9 @@
use crate::calendar::resource::{CalendarResource, CalendarResourceService};
use crate::calendar::resource::CalendarResource;
use crate::{CalDavPrincipalUri, Error};
use actix_web::web;
use async_trait::async_trait;
use rustical_dav::extensions::{CommonPropertiesExtension, CommonPropertiesProp};
use rustical_dav::privileges::UserPrivilegeSet;
use rustical_dav::resource::{PrincipalUri, Resource, ResourceService};
use rustical_dav::resource::{AxumMethods, PrincipalUri, Resource, ResourceService};
use rustical_dav::xml::{Resourcetype, ResourcetypeInner};
use rustical_store::auth::User;
use rustical_store::{CalendarStore, SubscriptionStore};
@@ -67,6 +66,16 @@ pub struct CalendarSetResourceService<C: CalendarStore, S: SubscriptionStore> {
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 {
@@ -116,16 +125,5 @@ impl<C: CalendarStore, S: SubscriptionStore> ResourceService for CalendarSetReso
})
.collect())
}
fn actix_scope(self) -> actix_web::Scope {
web::scope(&format!("/{}", self.name))
.service(
CalendarResourceService::<_, S>::new(
self.cal_store.clone(),
self.sub_store.clone(),
)
.actix_scope(),
)
.service(self.actix_resource())
}
}
impl<C: CalendarStore, S: SubscriptionStore> AxumMethods for CalendarSetResourceService<C, S> {}

View File

@@ -1,7 +1,9 @@
use actix_web::{
HttpResponse,
http::{StatusCode, header::ContentType},
use axum::{
body::Body,
response::{IntoResponse, Response},
};
use headers::{ContentType, HeaderMapExt};
use http::StatusCode;
use rustical_xml::{XmlSerialize, XmlSerializeRoot};
use tracing::error;
@@ -12,22 +14,18 @@ pub enum Precondition {
ValidCalendarData,
}
impl actix_web::ResponseError for Precondition {
fn status_code(&self) -> StatusCode {
StatusCode::PRECONDITION_FAILED
}
fn error_response(&self) -> HttpResponse<actix_web::body::BoxBody> {
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);
let error = rustical_dav::xml::ErrorElement(&self);
if let Err(err) = error.serialize_root(&mut writer) {
return rustical_dav::Error::from(err).error_response();
return rustical_dav::Error::from(err).into_response();
}
HttpResponse::PreconditionFailed()
.content_type(ContentType::xml())
.body(String::from_utf8(output).unwrap())
let mut res = Response::builder().status(StatusCode::PRECONDITION_FAILED);
res.headers_mut().unwrap().typed_insert(ContentType::xml());
res.body(Body::from(output)).unwrap()
}
}
@@ -61,8 +59,8 @@ pub enum Error {
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,
@@ -78,16 +76,13 @@ impl actix_web::ResponseError for Error {
Error::NotImplemented => StatusCode::INTERNAL_SERVER_ERROR,
Error::NotFound => StatusCode::NOT_FOUND,
Error::IcalError(err) => err.status_code(),
Error::PreconditionFailed(err) => err.status_code(),
}
}
fn error_response(&self) -> actix_web::HttpResponse<actix_web::body::BoxBody> {
error!("Error: {self}");
match self {
Error::DavError(err) => err.error_response(),
Error::IcalError(err) => err.error_response(),
Error::PreconditionFailed(err) => err.error_response(),
_ => HttpResponse::build(self.status_code()).body(self.to_string()),
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()
}
}

View File

@@ -1,28 +1,26 @@
use actix_web::HttpResponse;
use actix_web::body::BoxBody;
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::Data;
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;
use crate::calendar::resource::CalendarResourceService;
use crate::calendar_object::resource::CalendarObjectResourceService;
use crate::calendar_set::CalendarSetResourceService;
#[derive(Debug, Clone, Constructor)]
pub struct CalDavPrincipalUri(&'static str);
@@ -32,33 +30,38 @@ impl PrincipalUri for CalDavPrincipalUri {
}
}
/// Quite a janky implementation but the default METHOD_NOT_ALLOWED response gives us the allowed
/// methods of a resource
fn options_handler() -> ErrorHandlers<BoxBody> {
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, webdav-push",
),
));
// pub fn caldav_service<
// 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 {
// let birthday_store = Arc::new(ContactBirthdayStore::new(addr_store));
//
// RootResourceService::<_, User, CalDavPrincipalUri>::new(PrincipalResourceService {
// auth_provider: auth_provider.clone(),
// sub_store: subscription_store.clone(),
// birthday_store: birthday_store.clone(),
// cal_store: store.clone(),
// })
// .actix_scope()
// .wrap(AuthenticationMiddleware::new(auth_provider.clone()))
// .wrap(options_handler())
// .app_data(Data::from(store.clone()))
// .app_data(Data::from(birthday_store.clone()))
// .app_data(Data::new(CalDavPrincipalUri::new(
// format!("{prefix}/principal").leak(),
// )))
// .service(subscription_resource(subscription_store))
// }
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()
},
))
})
}
pub fn caldav_service<
pub fn caldav_router<
AP: AuthenticationProvider,
AS: AddressbookStore,
C: CalendarStore,
@@ -69,22 +72,53 @@ pub fn caldav_service<
store: Arc<C>,
addr_store: Arc<AS>,
subscription_store: Arc<S>,
) -> impl HttpServiceFactory {
) -> Router {
let birthday_store = Arc::new(ContactBirthdayStore::new(addr_store));
RootResourceService::<_, User, CalDavPrincipalUri>::new(PrincipalResourceService {
let principal_service = PrincipalResourceService {
auth_provider: auth_provider.clone(),
sub_store: subscription_store.clone(),
birthday_store: birthday_store.clone(),
cal_store: store.clone(),
})
.actix_scope()
.wrap(AuthenticationMiddleware::new(auth_provider.clone()))
.wrap(options_handler())
.app_data(Data::from(store.clone()))
.app_data(Data::from(birthday_store.clone()))
.app_data(Data::new(CalDavPrincipalUri::new(
format!("{prefix}/principal").leak(),
)))
.service(subscription_resource(subscription_store))
};
Router::new()
.route_service(
"/",
RootResourceService::<_, User, CalDavPrincipalUri>::new(principal_service.clone())
.axum_service(),
)
.route_service("/principal/{principal}", principal_service.axum_service())
.route_service(
"/principal/{principal}/calendar",
CalendarSetResourceService::new("calendar", store.clone(), subscription_store.clone())
.axum_service(),
)
.route_service(
"/principal/{principal}/calendar/{calendar_id}",
CalendarResourceService::new(store.clone(), subscription_store.clone()).axum_service(),
)
.route_service(
"/principal/{principal}/calendar/{calendar_id}/{object_id}",
CalendarObjectResourceService::new(store.clone()).axum_service(),
)
.route_service(
"/principal/{principal}/birthdays",
CalendarSetResourceService::new(
"birthdays",
birthday_store.clone(),
subscription_store.clone(),
)
.axum_service(),
)
.route_service(
"/principal/{principal}/birthdays/{calendar_id}",
CalendarResourceService::new(birthday_store.clone(), subscription_store.clone())
.axum_service(),
)
.route_service(
"/principal/{principal}/birthdays/{calendar_id}/{object_id}",
CalendarObjectResourceService::new(birthday_store.clone()).axum_service(),
)
.layer(AuthenticationLayer::new(auth_provider))
.layer(Extension(CalDavPrincipalUri(prefix)))
}

View File

@@ -1,10 +1,9 @@
use crate::calendar_set::{CalendarSetResource, CalendarSetResourceService};
use crate::calendar_set::CalendarSetResource;
use crate::{CalDavPrincipalUri, Error};
use actix_web::web;
use async_trait::async_trait;
use rustical_dav::extensions::{CommonPropertiesExtension, CommonPropertiesProp};
use rustical_dav::privileges::UserPrivilegeSet;
use rustical_dav::resource::{PrincipalUri, Resource, ResourceService};
use rustical_dav::resource::{AxumMethods, PrincipalUri, Resource, ResourceService};
use rustical_dav::xml::{HrefElement, Resourcetype, ResourcetypeInner};
use rustical_store::auth::user::PrincipalType;
use rustical_store::auth::{AuthenticationProvider, User};
@@ -194,25 +193,9 @@ impl<AP: AuthenticationProvider, S: SubscriptionStore, CS: CalendarStore, BS: Ca
),
])
}
fn actix_scope(self) -> actix_web::Scope {
web::scope("/principal/{principal}")
.service(
CalendarSetResourceService::<_, S>::new(
"calendar",
self.cal_store.clone(),
self.sub_store.clone(),
)
.actix_scope(),
)
.service(
CalendarSetResourceService::<_, S>::new(
"birthdays",
self.birthday_store.clone(),
self.sub_store.clone(),
)
.actix_scope(),
)
.service(self.actix_resource())
}
}
impl<AP: AuthenticationProvider, S: SubscriptionStore, CS: CalendarStore, BS: CalendarStore>
AxumMethods for PrincipalResourceService<AP, S, CS, BS>
{
}