mirror of
https://github.com/lennart-k/rustical.git
synced 2025-12-14 07:02:24 +00:00
Refactor all the propfind business
This commit is contained in:
@@ -7,7 +7,7 @@ use principal::PrincipalResource;
|
|||||||
use root::RootResource;
|
use root::RootResource;
|
||||||
use rustical_auth::CheckAuthentication;
|
use rustical_auth::CheckAuthentication;
|
||||||
use rustical_dav::error::Error;
|
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 rustical_store::calendar::CalendarStore;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -48,15 +48,15 @@ pub fn configure_dav<A: CheckAuthentication, C: CalendarStore + ?Sized>(
|
|||||||
.guard(guard::Method(Method::OPTIONS))
|
.guard(guard::Method(Method::OPTIONS))
|
||||||
.to(options_handler),
|
.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(
|
.service(
|
||||||
web::resource("/{principal}")
|
web::resource("/{principal}")
|
||||||
.route(propfind_method().to(handle_propfind::<A, PrincipalResource<C>>)),
|
.route(propfind_method().to(route_propfind::<A, PrincipalResource<C>>)),
|
||||||
)
|
)
|
||||||
.service(
|
.service(
|
||||||
web::resource("/{principal}/{calendar}")
|
web::resource("/{principal}/{calendar}")
|
||||||
.route(report_method().to(calendar::methods::route_report_calendar::<A, C>))
|
.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(
|
.route(
|
||||||
mkcalendar_method().to(calendar::methods::mkcalendar::route_mkcol_calendar::<A, C>),
|
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(
|
.service(
|
||||||
web::resource("/{principal}/{calendar}/{event}")
|
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::DELETE).to(event::methods::delete_event::<A, C>))
|
||||||
.route(web::method(Method::GET).to(event::methods::get_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>)),
|
.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::namespace::Namespace;
|
||||||
use crate::resource::HandlePropfind;
|
use crate::resource::HandlePropfind;
|
||||||
use crate::resource::ResourceService;
|
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::header::ContentType;
|
||||||
use actix_web::http::StatusCode;
|
|
||||||
use actix_web::web::{Data, Path};
|
use actix_web::web::{Data, Path};
|
||||||
use actix_web::{HttpRequest, HttpResponse};
|
use actix_web::{HttpRequest, HttpResponse};
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::Result;
|
||||||
use rustical_auth::{AuthInfoExtractor, CheckAuthentication};
|
use rustical_auth::{AuthInfoExtractor, CheckAuthentication};
|
||||||
|
use serde::Deserialize;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
// This is not the final place for this struct
|
// This is not the final place for this struct
|
||||||
pub struct ServicePrefix(pub String);
|
pub struct ServicePrefix(pub String);
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Deserialize, Clone, Debug)]
|
||||||
pub enum Error {
|
#[serde(rename_all = "kebab-case")]
|
||||||
#[error("invalid propfind request: {0}")]
|
struct PropElement {
|
||||||
InvalidPropfind(&'static str),
|
#[serde(flatten)]
|
||||||
#[error("input is not valid xml")]
|
prop: TagList,
|
||||||
ParsingError(#[from] roxmltree::Error),
|
|
||||||
#[error(transparent)]
|
|
||||||
Other(#[from] anyhow::Error),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl actix_web::error::ResponseError for Error {
|
#[derive(Deserialize, Clone, Debug)]
|
||||||
fn status_code(&self) -> actix_web::http::StatusCode {
|
#[serde(rename_all = "kebab-case")]
|
||||||
match self {
|
enum PropfindType {
|
||||||
Self::InvalidPropfind(_) => StatusCode::BAD_REQUEST,
|
Propname,
|
||||||
Self::ParsingError(_) => StatusCode::BAD_REQUEST,
|
Allprop,
|
||||||
Self::Other(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
Prop(PropElement),
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn error_response(&self) -> HttpResponse<actix_web::body::BoxBody> {
|
|
||||||
HttpResponse::build(self.status_code()).body(self.to_string())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_propfind(body: &str) -> Result<Vec<&str>, Error> {
|
#[derive(Deserialize, Clone, Debug)]
|
||||||
if body.is_empty() {
|
#[serde(rename_all = "kebab-case")]
|
||||||
// if body is empty, allprops must be returned (RFC 4918)
|
struct PropfindElement {
|
||||||
return Ok(vec!["allprops"]);
|
#[serde(rename = "$value")]
|
||||||
}
|
prop: PropfindType,
|
||||||
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(Serialize)]
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename = "multistatus")]
|
||||||
struct MultistatusElement<T1: Serialize, T2: Serialize> {
|
struct MultistatusElement<T1: Serialize, T2: Serialize> {
|
||||||
#[serde(rename = "$value")]
|
response: T1,
|
||||||
responses: Vec<T1>,
|
#[serde(rename = "response")]
|
||||||
#[serde(rename = "$value")]
|
|
||||||
member_responses: Vec<T2>,
|
member_responses: Vec<T2>,
|
||||||
#[serde(rename = "@xmlns")]
|
#[serde(rename = "@xmlns")]
|
||||||
ns_dav: &'static str,
|
ns_dav: &'static str,
|
||||||
@@ -128,3 +49,58 @@ struct MultistatusElement<T1: Serialize, T2: Serialize> {
|
|||||||
#[serde(rename = "@xmlns:IC")]
|
#[serde(rename = "@xmlns:IC")]
|
||||||
ns_ical: &'static str,
|
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 actix_web::{http::StatusCode, HttpRequest};
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
@@ -10,7 +10,7 @@ use strum::VariantNames;
|
|||||||
|
|
||||||
#[async_trait(?Send)]
|
#[async_trait(?Send)]
|
||||||
pub trait Resource {
|
pub trait Resource {
|
||||||
type PropType: FromStr + VariantNames + Into<&'static str> + Clone;
|
type PropType: FromStr + VariantNames + Clone;
|
||||||
type PropResponse: Serialize;
|
type PropResponse: Serialize;
|
||||||
|
|
||||||
fn list_dead_props() -> &'static [&'static str] {
|
fn list_dead_props() -> &'static [&'static str] {
|
||||||
@@ -82,12 +82,19 @@ impl<R: Resource> HandlePropfind for R {
|
|||||||
prefix: &str,
|
prefix: &str,
|
||||||
props: Vec<&str>,
|
props: Vec<&str>,
|
||||||
) -> Result<PropstatResponseElement<PropWrapper<Vec<R::PropResponse>>, TagList>> {
|
) -> Result<PropstatResponseElement<PropWrapper<Vec<R::PropResponse>>, TagList>> {
|
||||||
// TODO: implement propname
|
let mut props = props;
|
||||||
let mut props = props.into_iter().unique().collect_vec();
|
if props.contains(&"propname") {
|
||||||
if props.contains(&"allprops") {
|
|
||||||
if props.len() != 1 {
|
if props.len() != 1 {
|
||||||
// allprops MUST be the only queried prop per spec
|
// propname MUST be the only queried prop per spec
|
||||||
return Err(anyhow!("allprops MUST be the only queried prop"));
|
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();
|
props = R::list_dead_props().into();
|
||||||
}
|
}
|
||||||
@@ -103,17 +110,20 @@ impl<R: Resource> HandlePropfind for R {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut propstats = Vec::new();
|
let mut propstats = vec![PropstatType::Normal(PropstatElement {
|
||||||
propstats.push(PropstatType::Normal(PropstatElement {
|
|
||||||
status: format!("HTTP/1.1 {}", StatusCode::OK),
|
status: format!("HTTP/1.1 {}", StatusCode::OK),
|
||||||
prop: PropWrapper {
|
prop: PropWrapper {
|
||||||
prop: prop_responses,
|
prop: prop_responses,
|
||||||
},
|
},
|
||||||
}));
|
})];
|
||||||
if !invalid_props.is_empty() {
|
if !invalid_props.is_empty() {
|
||||||
propstats.push(PropstatType::NotFound(PropstatElement {
|
propstats.push(PropstatType::NotFound(PropstatElement {
|
||||||
status: format!("HTTP/1.1 {}", StatusCode::NOT_FOUND),
|
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 {
|
Ok(PropstatResponseElement {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use quick_xml::{events::attributes::Attribute, Writer};
|
use quick_xml::{events::attributes::Attribute, Writer};
|
||||||
use serde::ser::SerializeMap;
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
@@ -16,21 +15,6 @@ impl HrefElement {
|
|||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
pub struct TextNode(pub Option<String>);
|
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>
|
pub fn generate_multistatus<'a, F, A>(namespaces: A, closure: F) -> Result<String>
|
||||||
where
|
where
|
||||||
F: FnOnce(&mut Writer<&mut Vec<u8>>) -> Result<(), quick_xml::Error>,
|
F: FnOnce(&mut Writer<&mut Vec<u8>>) -> Result<(), quick_xml::Error>,
|
||||||
|
|||||||
Reference in New Issue
Block a user