Initial commit

This commit is contained in:
Lennart
2023-09-04 13:20:13 +02:00
commit ccb09f40b4
26 changed files with 10064 additions and 0 deletions

View File

@@ -0,0 +1,207 @@
use crate::namespace::Namespace;
use crate::propfind::{
generate_multistatus, parse_propfind, write_invalid_props_response, write_propstat_response,
write_resourcetype,
};
use crate::Error;
use actix_web::http::header::ContentType;
use actix_web::http::StatusCode;
use actix_web::web::{Data, Path};
use actix_web::{HttpRequest, HttpResponse};
use actix_web_httpauth::extractors::basic::BasicAuth;
use anyhow::Result;
use quick_xml::events::BytesText;
use quick_xml::Writer;
use roxmltree::{Node, NodeType};
use rustical_store::calendar::{Calendar, CalendarStore, Event};
use std::collections::HashSet;
use std::ops::Deref;
use std::sync::Arc;
use tokio::sync::RwLock;
async fn handle_report_calendar_query(
query_node: Node<'_, '_>,
request: HttpRequest,
events: Vec<Event>,
) -> Result<HttpResponse, Error> {
let prop_node = query_node
.children()
.find(|n| n.node_type() == NodeType::Element && n.tag_name().name() == "prop")
.ok_or(Error::BadRequest)?;
let props: Arc<HashSet<&str>> = Arc::new(
prop_node
.children()
.map(|node| node.tag_name().name())
.collect(),
);
let output = generate_multistatus(vec![Namespace::Dav, Namespace::CalDAV], |writer| {
for event in events {
write_propstat_response(
writer,
&format!("{}/{}", request.path(), event.get_uid()),
StatusCode::OK,
|writer| {
for prop in props.deref() {
match *prop {
"getetag" => {
writer
.create_element("getetag")
.write_text_content(BytesText::new(&event.get_etag()))?;
}
"calendar-data" => {
writer
.create_element("C:calendar-data")
.write_text_content(BytesText::new(event.to_ics()))?;
}
_ => {}
}
}
Ok(())
},
)?;
}
Ok(())
})
.map_err(|_e| Error::InternalError)?;
Ok(HttpResponse::MultiStatus()
.content_type(ContentType::xml())
.body(output))
}
pub async fn route_report_calendar<C: CalendarStore>(
store: Data<RwLock<C>>,
body: String,
path: Path<(String, String)>,
request: HttpRequest,
) -> Result<HttpResponse, Error> {
let (_principal, cid) = path.into_inner();
let doc = roxmltree::Document::parse(&body).map_err(|_e| Error::InternalError)?;
let query_node = doc.root_element();
let events = store.read().await.get_events(&cid).await.unwrap();
// TODO: implement filtering
match query_node.tag_name().name() {
"calendar-query" => {}
"calendar-multiget" => {}
_ => return Err(Error::BadRequest),
};
handle_report_calendar_query(query_node, request, events).await
}
pub fn generate_propfind_calendar_response(
props: Vec<&str>,
principal: &str,
path: &str,
calendar: &Calendar,
) -> Result<String> {
let mut props = props;
if props.contains(&"allprops") {
props.extend(
[
"resourcetype",
"current-user-principal",
"displayname",
"supported-calendar-component-set",
"getcontenttype",
"calendar-description",
]
.iter(),
);
}
let mut invalid_props = Vec::<&str>::new();
let mut output_buffer = Vec::new();
let mut writer = Writer::new_with_indent(&mut output_buffer, b' ', 2);
write_propstat_response(&mut writer, path, StatusCode::OK, |writer| {
for prop in props {
match prop {
"resourcetype" => write_resourcetype(writer, vec!["C:calendar", "collection"])?,
"current-user-principal" => {
writer
.create_element("current-user-principal")
.write_inner_content(|writer| {
writer
.create_element("href")
.write_text_content(BytesText::new(&format!(
// TODO: Replace hard-coded string
"/dav/{principal}/",
)))?;
Ok(())
})?;
}
"displayname" => {
let el = writer.create_element("displayname");
if let Some(name) = calendar.clone().name {
el.write_text_content(BytesText::new(&name))?;
} else {
el.write_empty()?;
}
}
"supported-calendar-component-set" => {
writer
.create_element("C:supported-calendar-component-set")
.write_inner_content(|writer| {
writer
.create_element("C:comp")
.with_attribute(("name", "VEVENT"))
.write_empty()?;
Ok(())
})?;
}
"getcontenttype" => {
writer
.create_element("getcontenttype")
.write_text_content(BytesText::new("text/calendar"))?;
}
"allprops" => {}
_ => invalid_props.push(prop),
};
}
Ok(())
})?;
write_invalid_props_response(&mut writer, path, invalid_props)?;
Ok(std::str::from_utf8(&output_buffer)?.to_string())
}
pub async fn route_propfind_calendar<C: CalendarStore>(
path: Path<(String, String)>,
body: String,
request: HttpRequest,
auth: BasicAuth,
store: Data<RwLock<C>>,
) -> Result<HttpResponse, Error> {
let (_principal, cid) = path.into_inner();
let calendar = store
.read()
.await
.get_calendar(&cid)
.await
.map_err(|_e| Error::InternalError)?;
let props = parse_propfind(&body).map_err(|_e| Error::BadRequest)?;
let responses_string = generate_propfind_calendar_response(
props.clone(),
auth.user_id(),
request.path(),
&calendar,
)
.map_err(|_e| Error::InternalError)?;
let output = generate_multistatus(vec![Namespace::Dav, Namespace::CalDAV], |writer| {
writer.write_event(quick_xml::events::Event::Text(BytesText::from_escaped(
responses_string,
)))?;
Ok(())
})
.map_err(|_e| Error::InternalError)?;
Ok(HttpResponse::MultiStatus()
.content_type(ContentType::xml())
.body(output))
}

View File

@@ -0,0 +1,64 @@
use crate::Error;
use actix_web::web::{Data, Path};
use actix_web::HttpResponse;
use rustical_store::calendar::CalendarStore;
use tokio::sync::RwLock;
pub async fn delete_event<C: CalendarStore>(
store: Data<RwLock<C>>,
path: Path<(String, String, String)>,
) -> Result<HttpResponse, Error> {
let (_principal, mut cid, uid) = path.into_inner();
if cid.ends_with(".ics") {
cid.truncate(cid.len() - 4);
}
store
.write()
.await
.delete_event(&uid)
.await
.map_err(|_e| Error::InternalError)?;
Ok(HttpResponse::Ok().body(""))
}
pub async fn get_event<C: CalendarStore>(
store: Data<RwLock<C>>,
path: Path<(String, String, String)>,
) -> Result<HttpResponse, Error> {
let (_principal, mut cid, uid) = path.into_inner();
if cid.ends_with(".ics") {
cid.truncate(cid.len() - 4);
}
let event = store
.read()
.await
.get_event(&uid)
.await
.map_err(|_e| Error::NotFound)?;
Ok(HttpResponse::Ok()
.insert_header(("ETag", event.get_etag()))
.body(event.to_ics().to_string()))
}
pub async fn put_event<C: CalendarStore>(
store: Data<RwLock<C>>,
path: Path<(String, String, String)>,
body: String,
) -> Result<HttpResponse, Error> {
let (_principal, mut cid, uid) = path.into_inner();
// Incredibly bodged method of normalising the uid but works for a prototype
if cid.ends_with(".ics") {
cid.truncate(cid.len() - 4);
}
dbg!(&body);
store
.write()
.await
.upsert_event(uid, body)
.await
.map_err(|_e| Error::InternalError)?;
Ok(HttpResponse::Ok().body(""))
}

View File

@@ -0,0 +1,4 @@
pub mod calendar;
pub mod event;
pub mod principal;
pub mod root;

View File

@@ -0,0 +1,140 @@
use crate::{
calendar::generate_propfind_calendar_response,
depth_extractor::Depth,
namespace::Namespace,
propfind::{
generate_multistatus, parse_propfind, write_invalid_props_response,
write_propstat_response, write_resourcetype,
},
Error,
};
use actix_web::{
http::{header::ContentType, StatusCode},
web::Data,
HttpRequest, HttpResponse,
};
use actix_web_httpauth::extractors::basic::BasicAuth;
use anyhow::Result;
use quick_xml::{
events::{BytesText, Event},
Writer,
};
use rustical_store::calendar::CalendarStore;
use tokio::sync::RwLock;
// Executes the PROPFIND request and returns a XML string to be written into a <mulstistatus> object.
pub async fn generate_propfind_principal_response(
props: Vec<&str>,
principal: &str,
path: &str,
) -> Result<String, quick_xml::Error> {
let mut props = props;
if props.contains(&"allprops") {
props.extend(
[
"resourcetype",
"current-user-principal",
"principal-URL",
"calendar-home-set",
"calendar-user-address-set",
]
.iter(),
);
}
let mut invalid_props = Vec::<&str>::new();
let mut output_buffer = Vec::new();
let mut writer = Writer::new_with_indent(&mut output_buffer, b' ', 2);
write_propstat_response(&mut writer, path, StatusCode::OK, |writer| {
let writer = writer;
for prop in props {
match prop {
"resourcetype" => write_resourcetype(writer, vec!["principal", "collection"])?,
"current-user-principal" | "principal-URL" => {
writer.create_element(prop).write_inner_content(|writer| {
writer
.create_element("href")
// TODO: Replace hard-coded string
.write_text_content(BytesText::new(&format!("/dav/{principal}/",)))?;
Ok(())
})?;
}
"calendar-home-set" | "calendar-user-address-set" => {
writer
.create_element(&format!("C:{prop}"))
.write_inner_content(|writer| {
writer
.create_element("href")
.write_text_content(BytesText::new(&format!(
// TODO: Replace hard-coded string
"/dav/{principal}/"
)))?;
Ok(())
})?;
}
"allprops" => {}
_ => invalid_props.push(prop),
};
}
Ok(())
})?;
dbg!(&invalid_props);
write_invalid_props_response(&mut writer, path, invalid_props)?;
Ok(std::str::from_utf8(&output_buffer)?.to_string())
}
pub async fn route_propfind_principal<C: CalendarStore>(
body: String,
request: HttpRequest,
auth: BasicAuth,
store: Data<RwLock<C>>,
depth: Depth,
) -> Result<HttpResponse, Error> {
let props = parse_propfind(&body).map_err(|_e| Error::BadRequest)?;
let mut responses = Vec::new();
// also get calendars:
if depth != Depth::Zero {
let cals = store
.read()
.await
.get_calendars()
.await
.map_err(|_e| Error::InternalError)?;
for cal in cals {
responses.push(
generate_propfind_calendar_response(
props.clone(),
auth.user_id(),
&format!("{}/{}", request.path(), cal.id),
&cal,
)
.map_err(|_e| Error::InternalError)?,
);
}
}
responses.push(
generate_propfind_principal_response(props.clone(), auth.user_id(), request.path())
.await
.map_err(|_e| Error::InternalError)?,
);
let output = generate_multistatus(vec![Namespace::Dav, Namespace::CalDAV], |writer| {
for response in responses {
writer.write_event(Event::Text(BytesText::from_escaped(response)))?;
}
Ok(())
})
.map_err(|_e| Error::InternalError)?;
println!("{}", &output);
Ok(HttpResponse::MultiStatus()
.content_type(ContentType::xml())
.body(output))
}

View File

@@ -0,0 +1,85 @@
use actix_web::{
http::{header::ContentType, StatusCode},
HttpRequest, HttpResponse,
};
use actix_web_httpauth::extractors::basic::BasicAuth;
use quick_xml::{
events::{BytesText, Event},
Writer,
};
use crate::{
namespace::Namespace,
propfind::{
generate_multistatus, parse_propfind, write_invalid_props_response,
write_propstat_response, write_resourcetype,
},
Error,
};
// Executes the PROPFIND request and returns a XML string to be written into a <mulstistatus> object.
pub async fn generate_propfind_root_response(
props: Vec<&str>,
principal: &str,
path: &str,
) -> Result<String, quick_xml::Error> {
let mut props = props;
if props.contains(&"allprops") {
props.extend(["resourcetype", "current-user-principal"].iter());
}
let mut invalid_props = Vec::<&str>::new();
let mut output_buffer = Vec::new();
let mut writer = Writer::new_with_indent(&mut output_buffer, b' ', 2);
write_propstat_response(&mut writer, path, StatusCode::OK, |writer| {
for prop in props {
match prop {
"resourcetype" => write_resourcetype(writer, vec!["collection"])?,
"current-user-principal" => {
writer
.create_element("current-user-principal")
.write_inner_content(|writer| {
writer
.create_element("href")
.write_text_content(BytesText::new(
// TODO: Replace hard-coded string
&format!("/dav/{principal}",),
))?;
Ok(())
})?;
}
"allprops" => {}
_ => invalid_props.push(prop),
};
}
Ok(())
})?;
dbg!(&invalid_props);
write_invalid_props_response(&mut writer, path, invalid_props)?;
Ok(std::str::from_utf8(&output_buffer)?.to_string())
}
pub async fn route_propfind_root(
body: String,
request: HttpRequest,
auth: BasicAuth,
) -> Result<HttpResponse, Error> {
let props = parse_propfind(&body).map_err(|_e| Error::BadRequest)?;
let responses_string =
generate_propfind_root_response(props.clone(), auth.user_id(), request.path())
.await
.map_err(|_e| Error::InternalError)?;
let output = generate_multistatus(vec![Namespace::Dav, Namespace::CalDAV], |writer| {
writer.write_event(Event::Text(BytesText::from_escaped(responses_string)))?;
Ok(())
})
.map_err(|_e| Error::InternalError)?;
Ok(HttpResponse::MultiStatus()
.content_type(ContentType::xml())
.body(output))
}