mirror of
https://github.com/lennart-k/rustical.git
synced 2025-12-13 22:52:22 +00:00
Refactor all the propfind business
This commit is contained in:
@@ -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>)),
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>,
|
||||
|
||||
Reference in New Issue
Block a user