Refactor all the propfind business

This commit is contained in:
Lennart
2024-05-29 13:57:23 +02:00
parent b2f415fb9d
commit 0f3c3d5beb
4 changed files with 103 additions and 133 deletions

View File

@@ -7,7 +7,7 @@ use principal::PrincipalResource;
use root::RootResource;
use rustical_auth::CheckAuthentication;
use rustical_dav::error::Error;
use rustical_dav::propfind::{handle_propfind, ServicePrefix};
use rustical_dav::propfind::{route_propfind, ServicePrefix};
use rustical_store::calendar::CalendarStore;
use std::str::FromStr;
use std::sync::Arc;
@@ -48,15 +48,15 @@ pub fn configure_dav<A: CheckAuthentication, C: CalendarStore + ?Sized>(
.guard(guard::Method(Method::OPTIONS))
.to(options_handler),
)
.service(web::resource("").route(propfind_method().to(handle_propfind::<A, RootResource>)))
.service(web::resource("").route(propfind_method().to(route_propfind::<A, RootResource>)))
.service(
web::resource("/{principal}")
.route(propfind_method().to(handle_propfind::<A, PrincipalResource<C>>)),
.route(propfind_method().to(route_propfind::<A, PrincipalResource<C>>)),
)
.service(
web::resource("/{principal}/{calendar}")
.route(report_method().to(calendar::methods::route_report_calendar::<A, C>))
.route(propfind_method().to(handle_propfind::<A, CalendarResource<C>>))
.route(propfind_method().to(route_propfind::<A, CalendarResource<C>>))
.route(
mkcalendar_method().to(calendar::methods::mkcalendar::route_mkcol_calendar::<A, C>),
)
@@ -67,7 +67,7 @@ pub fn configure_dav<A: CheckAuthentication, C: CalendarStore + ?Sized>(
)
.service(
web::resource("/{principal}/{calendar}/{event}")
.route(propfind_method().to(handle_propfind::<A, EventResource<C>>))
.route(propfind_method().to(route_propfind::<A, EventResource<C>>))
.route(web::method(Method::DELETE).to(event::methods::delete_event::<A, C>))
.route(web::method(Method::GET).to(event::methods::get_event::<A, C>))
.route(web::method(Method::PUT).to(event::methods::put_event::<A, C>)),

View File

@@ -2,124 +2,45 @@ use crate::depth_extractor::Depth;
use crate::namespace::Namespace;
use crate::resource::HandlePropfind;
use crate::resource::ResourceService;
use crate::xml_snippets::generate_multistatus;
use crate::xml::tag_list::TagList;
use actix_web::http::header::ContentType;
use actix_web::http::StatusCode;
use actix_web::web::{Data, Path};
use actix_web::{HttpRequest, HttpResponse};
use anyhow::{anyhow, Result};
use anyhow::Result;
use rustical_auth::{AuthInfoExtractor, CheckAuthentication};
use serde::Deserialize;
use serde::Serialize;
use thiserror::Error;
// This is not the final place for this struct
pub struct ServicePrefix(pub String);
#[derive(Debug, Error)]
pub enum Error {
#[error("invalid propfind request: {0}")]
InvalidPropfind(&'static str),
#[error("input is not valid xml")]
ParsingError(#[from] roxmltree::Error),
#[error(transparent)]
Other(#[from] anyhow::Error),
#[derive(Deserialize, Clone, Debug)]
#[serde(rename_all = "kebab-case")]
struct PropElement {
#[serde(flatten)]
prop: TagList,
}
impl actix_web::error::ResponseError for Error {
fn status_code(&self) -> actix_web::http::StatusCode {
match self {
Self::InvalidPropfind(_) => StatusCode::BAD_REQUEST,
Self::ParsingError(_) => StatusCode::BAD_REQUEST,
Self::Other(_) => StatusCode::INTERNAL_SERVER_ERROR,
}
}
fn error_response(&self) -> HttpResponse<actix_web::body::BoxBody> {
HttpResponse::build(self.status_code()).body(self.to_string())
}
#[derive(Deserialize, Clone, Debug)]
#[serde(rename_all = "kebab-case")]
enum PropfindType {
Propname,
Allprop,
Prop(PropElement),
}
pub fn parse_propfind(body: &str) -> Result<Vec<&str>, Error> {
if body.is_empty() {
// if body is empty, allprops must be returned (RFC 4918)
return Ok(vec!["allprops"]);
}
let doc = roxmltree::Document::parse(body)?;
let propfind_node = doc.root_element();
if propfind_node.tag_name().name() != "propfind" {
return Err(Error::InvalidPropfind("root tag is not <propfind>"));
}
let prop_node = if let Some(el) = propfind_node.first_element_child() {
el
} else {
return Ok(Vec::new());
};
match prop_node.tag_name().name() {
"prop" => Ok(prop_node
.children()
.filter(|node| node.is_element())
.map(|node| node.tag_name().name())
.collect()),
_ => Err(Error::InvalidPropfind(
"invalid tag in <propfind>, expected <prop>",
)),
}
}
pub async fn handle_propfind<A: CheckAuthentication, R: ResourceService + ?Sized>(
path: Path<R::PathComponents>,
body: String,
req: HttpRequest,
prefix: Data<ServicePrefix>,
auth: AuthInfoExtractor<A>,
depth: Depth,
) -> Result<HttpResponse, crate::error::Error> {
// TODO: fix errors
let props = parse_propfind(&body).map_err(|_e| anyhow!("propfind parsing error"))?;
let auth_info = auth.inner;
let prefix = prefix.0.to_owned();
let path_components = path.into_inner();
let resource_service = R::new(req, auth_info.clone(), path_components.clone()).await?;
let resource = resource_service.get_file().await?;
let response = resource.propfind(&prefix, props.clone()).await?;
let mut member_responses = Vec::new();
if depth != Depth::Zero {
for member in resource_service.get_members(auth_info).await? {
member_responses.push(member.propfind(&prefix, props.clone()).await?);
}
}
let output = generate_multistatus(
vec![Namespace::Dav, Namespace::CalDAV, Namespace::ICal],
|writer| {
writer
.write_serializable("response", &response)
.map_err(|_e| quick_xml::Error::TextNotFound)?;
for response in member_responses {
writer
.write_serializable("response", &response)
.map_err(|_e| quick_xml::Error::TextNotFound)?;
}
Ok(())
},
)?;
Ok(HttpResponse::MultiStatus()
.content_type(ContentType::xml())
.body(output))
#[derive(Deserialize, Clone, Debug)]
#[serde(rename_all = "kebab-case")]
struct PropfindElement {
#[serde(rename = "$value")]
prop: PropfindType,
}
#[derive(Serialize)]
#[serde(rename = "multistatus")]
struct MultistatusElement<T1: Serialize, T2: Serialize> {
#[serde(rename = "$value")]
responses: Vec<T1>,
#[serde(rename = "$value")]
response: T1,
#[serde(rename = "response")]
member_responses: Vec<T2>,
#[serde(rename = "@xmlns")]
ns_dav: &'static str,
@@ -128,3 +49,58 @@ struct MultistatusElement<T1: Serialize, T2: Serialize> {
#[serde(rename = "@xmlns:IC")]
ns_ical: &'static str,
}
pub async fn route_propfind<A: CheckAuthentication, R: ResourceService + ?Sized>(
path: Path<R::PathComponents>,
body: String,
req: HttpRequest,
prefix: Data<ServicePrefix>,
auth: AuthInfoExtractor<A>,
depth: Depth,
) -> Result<HttpResponse, crate::error::Error> {
let auth_info = auth.inner;
let prefix = prefix.0.to_owned();
let path_components = path.into_inner();
let resource_service = R::new(req, auth_info.clone(), path_components.clone()).await?;
let propfind: PropfindElement = quick_xml::de::from_str(&body).unwrap();
let props = match propfind.prop {
PropfindType::Allprop => {
vec!["allprop".to_owned()]
}
PropfindType::Propname => {
// TODO: Implement
return Err(crate::error::Error::InternalError);
}
PropfindType::Prop(PropElement { prop: prop_tags }) => prop_tags.into(),
};
let props: Vec<&str> = props.iter().map(String::as_str).collect();
let mut member_responses = Vec::new();
if depth != Depth::Zero {
for member in resource_service.get_members(auth_info).await? {
member_responses.push(member.propfind(&prefix, props.clone()).await?);
}
}
let resource = resource_service.get_file().await?;
let response = resource.propfind(&prefix, props).await?;
let mut output = String::new();
let mut ser = quick_xml::se::Serializer::new(&mut output);
ser.indent(' ', 4);
MultistatusElement {
response,
member_responses,
ns_dav: Namespace::Dav.as_str(),
ns_caldav: Namespace::CalDAV.as_str(),
ns_ical: Namespace::ICal.as_str(),
}
.serialize(ser)
.unwrap();
Ok(HttpResponse::MultiStatus()
.content_type(ContentType::xml())
.body(output))
}

View File

@@ -1,4 +1,4 @@
use crate::{error::Error, xml_snippets::TagList};
use crate::{error::Error, xml::tag_list::TagList};
use actix_web::{http::StatusCode, HttpRequest};
use anyhow::{anyhow, Result};
use async_trait::async_trait;
@@ -10,7 +10,7 @@ use strum::VariantNames;
#[async_trait(?Send)]
pub trait Resource {
type PropType: FromStr + VariantNames + Into<&'static str> + Clone;
type PropType: FromStr + VariantNames + Clone;
type PropResponse: Serialize;
fn list_dead_props() -> &'static [&'static str] {
@@ -82,12 +82,19 @@ impl<R: Resource> HandlePropfind for R {
prefix: &str,
props: Vec<&str>,
) -> Result<PropstatResponseElement<PropWrapper<Vec<R::PropResponse>>, TagList>> {
// TODO: implement propname
let mut props = props.into_iter().unique().collect_vec();
if props.contains(&"allprops") {
let mut props = props;
if props.contains(&"propname") {
if props.len() != 1 {
// allprops MUST be the only queried prop per spec
return Err(anyhow!("allprops MUST be the only queried prop"));
// propname MUST be the only queried prop per spec
return Err(anyhow!("propname MUST be the only queried prop"));
}
// TODO: implement propname
props = R::list_dead_props().into();
}
if props.contains(&"allprop") {
if props.len() != 1 {
// allprop MUST be the only queried prop per spec
return Err(anyhow!("allprop MUST be the only queried prop"));
}
props = R::list_dead_props().into();
}
@@ -103,17 +110,20 @@ impl<R: Resource> HandlePropfind for R {
}
}
let mut propstats = Vec::new();
propstats.push(PropstatType::Normal(PropstatElement {
let mut propstats = vec![PropstatType::Normal(PropstatElement {
status: format!("HTTP/1.1 {}", StatusCode::OK),
prop: PropWrapper {
prop: prop_responses,
},
}));
})];
if !invalid_props.is_empty() {
propstats.push(PropstatType::NotFound(PropstatElement {
status: format!("HTTP/1.1 {}", StatusCode::NOT_FOUND),
prop: TagList(invalid_props.iter().map(|&s| s.to_owned()).collect()),
prop: invalid_props
.into_iter()
.map(|s| s.to_owned())
.collect_vec()
.into(),
}));
}
Ok(PropstatResponseElement {

View File

@@ -1,6 +1,5 @@
use anyhow::Result;
use quick_xml::{events::attributes::Attribute, Writer};
use serde::ser::SerializeMap;
use serde::Serialize;
#[derive(Serialize)]
@@ -16,21 +15,6 @@ impl HrefElement {
#[derive(Serialize)]
pub struct TextNode(pub Option<String>);
pub struct TagList(pub Vec<String>);
impl Serialize for TagList {
fn serialize<S>(&self, serializer: S) -> std::prelude::v1::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut el = serializer.serialize_map(Some(self.0.len()))?;
for tag in &self.0 {
el.serialize_entry(&tag, &())?;
}
el.end()
}
}
pub fn generate_multistatus<'a, F, A>(namespaces: A, closure: F) -> Result<String>
where
F: FnOnce(&mut Writer<&mut Vec<u8>>) -> Result<(), quick_xml::Error>,