WIP: Add a CalDAV server #105
|
@ -4,3 +4,11 @@ env.sh
|
|||
aerogramme.toml
|
||||
*.swo
|
||||
*.swp
|
||||
aerogramme.pid
|
||||
cert.pem
|
||||
ec_key.pem
|
||||
provider-users.toml
|
||||
setup.toml
|
||||
test.eml
|
||||
test.txt
|
||||
users.toml
|
||||
|
|
File diff suppressed because it is too large
Load Diff
73
Cargo.toml
73
Cargo.toml
|
@ -1,15 +1,32 @@
|
|||
[package]
|
||||
name = "aerogramme"
|
||||
version = "0.2.2"
|
||||
authors = ["Alex Auvolat <alex@adnab.me>", "Quentin Dufour <quentin@dufour.io>"]
|
||||
edition = "2021"
|
||||
license = "EUPL-1.2"
|
||||
description = "A robust email server"
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"aero-user",
|
||||
"aero-bayou",
|
||||
"aero-sasl",
|
||||
"aero-dav",
|
||||
"aero-dav/fuzz",
|
||||
"aero-collections",
|
||||
"aero-proto",
|
||||
"aerogramme",
|
||||
]
|
||||
|
||||
default-members = ["aerogramme"]
|
||||
|
||||
[workspace.dependencies]
|
||||
# internal crates
|
||||
aero-user = { version = "0.3.0", path = "aero-user" }
|
||||
aero-bayou = { version = "0.3.0", path = "aero-bayou" }
|
||||
aero-sasl = { version = "0.3.0", path = "aero-sasl" }
|
||||
aero-dav = { version = "0.3.0", path = "aero-dav" }
|
||||
aero-collections = { version = "0.3.0", path = "aero-collections" }
|
||||
aero-proto = { version = "0.3.0", path = "aero-proto" }
|
||||
aerogramme = { version = "0.3.0", path = "aerogramme" }
|
||||
|
||||
[dependencies]
|
||||
# async runtime
|
||||
tokio = { version = "1.18", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "time", "macros", "sync", "signal", "fs"] }
|
||||
tokio = { version = "1.36", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "time", "macros", "sync", "signal", "fs"] }
|
||||
tokio-util = { version = "0.7", features = [ "compat" ] }
|
||||
tokio-stream = { version = "0.1" }
|
||||
futures = "0.3"
|
||||
|
||||
# debug
|
||||
|
@ -18,6 +35,7 @@ backtrace = "0.3"
|
|||
console-subscriber = "0.2"
|
||||
tracing-subscriber = "0.3"
|
||||
tracing = "0.1"
|
||||
thiserror = "1.0.56"
|
||||
|
||||
# language extensions
|
||||
lazy_static = "1.4"
|
||||
|
@ -32,13 +50,31 @@ chrono = { version = "0.4", default-features = false, features = ["alloc"] }
|
|||
nix = { version = "0.27", features = ["signal"] }
|
||||
clap = { version = "3.1.18", features = ["derive", "env"] }
|
||||
|
||||
# serialization & parsing
|
||||
# email protocols
|
||||
eml-codec = "0.1.2"
|
||||
smtp-message = { git = "http://github.com/Alexis211/kannader", branch = "feature/lmtp" }
|
||||
smtp-server = { git = "http://github.com/Alexis211/kannader", branch = "feature/lmtp" }
|
||||
imap-codec = { version = "2.0.0", features = ["bounded-static", "ext_condstore_qresync"] }
|
||||
imap-flow = { git = "https://github.com/duesee/imap-flow.git", branch = "main" }
|
||||
|
||||
# dav protocols
|
||||
icalendar = "0.16"
|
||||
|
||||
# http & web
|
||||
http = "1.1"
|
||||
http-body-util = "0.1.1"
|
||||
hyper = "1.2"
|
||||
hyper-rustls = { version = "0.26", features = ["http2"] }
|
||||
hyper-util = { version = "0.1", features = ["full"] }
|
||||
|
||||
# serialization, compression & parsing
|
||||
serde = "1.0.137"
|
||||
rmp-serde = "0.15"
|
||||
toml = "0.5"
|
||||
base64 = "0.21"
|
||||
hex = "0.4"
|
||||
nom = "7.1"
|
||||
quick-xml = { version = "0.31", features = ["async-tokio"] }
|
||||
zstd = { version = "0.9", default-features = false }
|
||||
|
||||
# cryptography & security
|
||||
|
@ -48,8 +84,6 @@ rand = "0.8.5"
|
|||
rustls = "0.22"
|
||||
rustls-pemfile = "2.0"
|
||||
tokio-rustls = "0.25"
|
||||
hyper-rustls = { version = "0.26", features = ["http2"] }
|
||||
hyper-util = { version = "0.1", features = ["full"] }
|
||||
rpassword = "7.0"
|
||||
|
||||
# login
|
||||
|
@ -62,21 +96,6 @@ aws-sdk-s3 = "1"
|
|||
aws-smithy-runtime = "1"
|
||||
aws-smithy-runtime-api = "1"
|
||||
|
||||
# email protocols
|
||||
eml-codec = "0.1.2"
|
||||
smtp-message = { git = "http://github.com/Alexis211/kannader", branch = "feature/lmtp" }
|
||||
smtp-server = { git = "http://github.com/Alexis211/kannader", branch = "feature/lmtp" }
|
||||
imap-codec = { version = "2.0.0", features = ["bounded-static", "ext_condstore_qresync"] }
|
||||
imap-flow = { git = "https://github.com/duesee/imap-flow.git", branch = "main" }
|
||||
thiserror = "1.0.56"
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
[patch.crates-io]
|
||||
imap-types = { git = "https://github.com/superboum/imap-codec", branch = "custom/aerogramme" }
|
||||
imap-codec = { git = "https://github.com/superboum/imap-codec", branch = "custom/aerogramme" }
|
||||
|
||||
[[test]]
|
||||
name = "behavior"
|
||||
path = "tests/behavior.rs"
|
||||
harness = false
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
[package]
|
||||
name = "aero-bayou"
|
||||
version = "0.3.0"
|
||||
authors = ["Alex Auvolat <alex@adnab.me>", "Quentin Dufour <quentin@dufour.io>"]
|
||||
edition = "2021"
|
||||
license = "EUPL-1.2"
|
||||
description = "A simplified version of Bayou by Terry et al. (ACM SIGOPS 1995)"
|
||||
|
||||
[dependencies]
|
||||
aero-user.workspace = true
|
||||
|
||||
anyhow.workspace = true
|
||||
hex.workspace = true
|
||||
tracing.workspace = true
|
||||
log.workspace = true
|
||||
rand.workspace = true
|
||||
serde.workspace = true
|
||||
tokio.workspace = true
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
pub mod timestamp;
|
||||
|
||||
use std::sync::{Arc, Weak};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
|
@ -7,9 +9,10 @@ use rand::prelude::*;
|
|||
use serde::{Deserialize, Serialize};
|
||||
use tokio::sync::{watch, Notify};
|
||||
|
||||
use crate::cryptoblob::*;
|
||||
use crate::login::Credentials;
|
||||
use crate::storage;
|
||||
use aero_user::cryptoblob::*;
|
||||
use aero_user::login::Credentials;
|
||||
use aero_user::storage;
|
||||
|
||||
use crate::timestamp::*;
|
||||
|
||||
const KEEP_STATE_EVERY: usize = 64;
|
|
@ -1,7 +1,8 @@
|
|||
use rand::prelude::*;
|
||||
use std::str::FromStr;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use rand::prelude::*;
|
||||
|
||||
/// Returns milliseconds since UNIX Epoch
|
||||
pub fn now_msec() -> u64 {
|
||||
SystemTime::now()
|
|
@ -0,0 +1,25 @@
|
|||
[package]
|
||||
name = "aero-collections"
|
||||
version = "0.3.0"
|
||||
authors = ["Alex Auvolat <alex@adnab.me>", "Quentin Dufour <quentin@dufour.io>"]
|
||||
edition = "2021"
|
||||
license = "EUPL-1.2"
|
||||
description = "Aerogramme own representation of the different objects it manipulates"
|
||||
|
||||
[dependencies]
|
||||
aero-user.workspace = true
|
||||
aero-bayou.workspace = true
|
||||
|
||||
anyhow.workspace = true
|
||||
base64.workspace = true
|
||||
futures.workspace = true
|
||||
lazy_static.workspace = true
|
||||
serde.workspace = true
|
||||
hex.workspace = true
|
||||
tokio.workspace = true
|
||||
tracing.workspace = true
|
||||
rand.workspace = true
|
||||
im.workspace = true
|
||||
sodiumoxide.workspace = true
|
||||
eml-codec.workspace = true
|
||||
icalendar.workspace = true
|
|
@ -0,0 +1,195 @@
|
|||
pub mod namespace;
|
||||
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use aero_bayou::Bayou;
|
||||
use aero_user::login::Credentials;
|
||||
use aero_user::cryptoblob::{self, gen_key, Key};
|
||||
use aero_user::storage::{self, BlobRef, BlobVal, Store};
|
||||
|
||||
use crate::unique_ident::*;
|
||||
use crate::davdag::{DavDag, IndexEntry, Token, BlobId, SyncChange};
|
||||
|
||||
pub struct Calendar {
|
||||
pub(super) id: UniqueIdent,
|
||||
internal: RwLock<CalendarInternal>,
|
||||
}
|
||||
|
||||
impl Calendar {
|
||||
pub(crate) async fn open(
|
||||
creds: &Credentials,
|
||||
id: UniqueIdent,
|
||||
) -> Result<Self> {
|
||||
let bayou_path = format!("calendar/dag/{}", id);
|
||||
let cal_path = format!("calendar/events/{}", id);
|
||||
|
||||
let mut davdag = Bayou::<DavDag>::new(creds, bayou_path).await?;
|
||||
davdag.sync().await?;
|
||||
|
||||
let internal = RwLock::new(CalendarInternal {
|
||||
id,
|
||||
encryption_key: creds.keys.master.clone(),
|
||||
storage: creds.storage.build().await?,
|
||||
davdag,
|
||||
cal_path,
|
||||
});
|
||||
|
||||
Ok(Self { id, internal })
|
||||
}
|
||||
|
||||
// ---- DAG sync utilities
|
||||
|
||||
/// Sync data with backing store
|
||||
pub async fn force_sync(&self) -> Result<()> {
|
||||
self.internal.write().await.force_sync().await
|
||||
}
|
||||
|
||||
/// Sync data with backing store only if changes are detected
|
||||
/// or last sync is too old
|
||||
pub async fn opportunistic_sync(&self) -> Result<()> {
|
||||
self.internal.write().await.opportunistic_sync().await
|
||||
}
|
||||
|
||||
// ---- Data API
|
||||
|
||||
/// Access the DAG internal data (you can get the list of files for example)
|
||||
pub async fn dag(&self) -> DavDag {
|
||||
// Cloning is cheap
|
||||
self.internal.read().await.davdag.state().clone()
|
||||
}
|
||||
|
||||
/// The diff API is a write API as we might need to push a merge node
|
||||
/// to get a new sync token
|
||||
pub async fn diff(&self, sync_token: Token) -> Result<(Token, Vec<SyncChange>)> {
|
||||
self.internal.write().await.diff(sync_token).await
|
||||
}
|
||||
|
||||
/// Get a specific event
|
||||
pub async fn get(&self, evt_id: UniqueIdent) -> Result<Vec<u8>> {
|
||||
self.internal.read().await.get(evt_id).await
|
||||
}
|
||||
|
||||
/// Put a specific event
|
||||
pub async fn put<'a>(&self, name: &str, evt: &'a [u8]) -> Result<(Token, IndexEntry)> {
|
||||
self.internal.write().await.put(name, evt).await
|
||||
}
|
||||
|
||||
/// Delete a specific event
|
||||
pub async fn delete(&self, blob_id: UniqueIdent) -> Result<Token> {
|
||||
self.internal.write().await.delete(blob_id).await
|
||||
}
|
||||
}
|
||||
|
||||
use base64::Engine;
|
||||
const MESSAGE_KEY: &str = "message-key";
|
||||
struct CalendarInternal {
|
||||
#[allow(dead_code)]
|
||||
id: UniqueIdent,
|
||||
cal_path: String,
|
||||
encryption_key: Key,
|
||||
storage: Store,
|
||||
davdag: Bayou<DavDag>,
|
||||
}
|
||||
|
||||
impl CalendarInternal {
|
||||
async fn force_sync(&mut self) -> Result<()> {
|
||||
self.davdag.sync().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn opportunistic_sync(&mut self) -> Result<()> {
|
||||
self.davdag.opportunistic_sync().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get(&self, blob_id: BlobId) -> Result<Vec<u8>> {
|
||||
// Fetch message from S3
|
||||
let blob_ref = storage::BlobRef(format!("{}/{}", self.cal_path, blob_id));
|
||||
let object = self.storage.blob_fetch(&blob_ref).await?;
|
||||
|
||||
// Decrypt message key from headers
|
||||
let key_encrypted_b64 = object
|
||||
.meta
|
||||
.get(MESSAGE_KEY)
|
||||
.ok_or(anyhow!("Missing key in metadata"))?;
|
||||
let key_encrypted = base64::engine::general_purpose::STANDARD.decode(key_encrypted_b64)?;
|
||||
let message_key_raw = cryptoblob::open(&key_encrypted, &self.encryption_key)?;
|
||||
let message_key =
|
||||
cryptoblob::Key::from_slice(&message_key_raw).ok_or(anyhow!("Invalid message key"))?;
|
||||
|
||||
// Decrypt body
|
||||
let body = object.value;
|
||||
cryptoblob::open(&body, &message_key)
|
||||
}
|
||||
|
||||
async fn put<'a>(&mut self, name: &str, evt: &'a [u8]) -> Result<(Token, IndexEntry)> {
|
||||
let message_key = gen_key();
|
||||
let blob_id = gen_ident();
|
||||
|
||||
let encrypted_msg_key = cryptoblob::seal(&message_key.as_ref(), &self.encryption_key)?;
|
||||
let key_header = base64::engine::general_purpose::STANDARD.encode(&encrypted_msg_key);
|
||||
|
||||
// Write event to S3
|
||||
let message_blob = cryptoblob::seal(evt, &message_key)?;
|
||||
let blob_val = BlobVal::new(
|
||||
BlobRef(format!("{}/{}", self.cal_path, blob_id)),
|
||||
message_blob,
|
||||
)
|
||||
.with_meta(MESSAGE_KEY.to_string(), key_header);
|
||||
|
||||
let etag = self.storage
|
||||
.blob_insert(blob_val)
|
||||
.await?;
|
||||
|
||||
// Add entry to Bayou
|
||||
let entry: IndexEntry = (blob_id, name.to_string(), etag);
|
||||
let davstate = self.davdag.state();
|
||||
let put_op = davstate.op_put(entry.clone());
|
||||
let token = put_op.token();
|
||||
self.davdag.push(put_op).await?;
|
||||
|
||||
Ok((token, entry))
|
||||
}
|
||||
|
||||
async fn delete(&mut self, blob_id: BlobId) -> Result<Token> {
|
||||
let davstate = self.davdag.state();
|
||||
|
||||
if !davstate.table.contains_key(&blob_id) {
|
||||
bail!("Cannot delete event that doesn't exist");
|
||||
}
|
||||
|
||||
let del_op = davstate.op_delete(blob_id);
|
||||
let token = del_op.token();
|
||||
self.davdag.push(del_op).await?;
|
||||
|
||||
let blob_ref = BlobRef(format!("{}/{}", self.cal_path, blob_id));
|
||||
self.storage.blob_rm(&blob_ref).await?;
|
||||
|
||||
Ok(token)
|
||||
}
|
||||
|
||||
async fn diff(&mut self, sync_token: Token) -> Result<(Token, Vec<SyncChange>)> {
|
||||
let davstate = self.davdag.state();
|
||||
|
||||
let token_changed = davstate.resolve(sync_token)?;
|
||||
let changes = token_changed
|
||||
.iter()
|
||||
.filter_map(|t: &Token| davstate.change.get(t))
|
||||
.map(|s| s.clone())
|
||||
.collect();
|
||||
|
||||
let heads = davstate.heads_vec();
|
||||
let token = match heads.as_slice() {
|
||||
[ token ] => *token,
|
||||
_ => {
|
||||
let op_mg = davstate.op_merge();
|
||||
let token = op_mg.token();
|
||||
self.davdag.push(op_mg).await?;
|
||||
token
|
||||
}
|
||||
};
|
||||
|
||||
Ok((token, changes))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,327 @@
|
|||
use anyhow::{bail, Result};
|
||||
use std::collections::{HashMap, BTreeMap};
|
||||
use std::sync::{Weak, Arc};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use aero_bayou::timestamp::now_msec;
|
||||
use aero_user::storage;
|
||||
use aero_user::cryptoblob::{open_deserialize, seal_serialize};
|
||||
|
||||
use crate::unique_ident::{gen_ident, UniqueIdent};
|
||||
use crate::user::User;
|
||||
use super::Calendar;
|
||||
|
||||
pub(crate) const CAL_LIST_PK: &str = "calendars";
|
||||
pub(crate) const CAL_LIST_SK: &str = "list";
|
||||
pub(crate) const MAIN_CAL: &str = "Personal";
|
||||
pub(crate) const MAX_CALNAME_CHARS: usize = 32;
|
||||
|
||||
pub struct CalendarNs(std::sync::Mutex<HashMap<UniqueIdent, Weak<Calendar>>>);
|
||||
|
||||
impl CalendarNs {
|
||||
/// Create a new calendar namespace
|
||||
pub fn new() -> Self {
|
||||
Self(std::sync::Mutex::new(HashMap::new()))
|
||||
}
|
||||
|
||||
/// Open a calendar by name
|
||||
pub async fn open(&self, user: &Arc<User>, name: &str) -> Result<Option<Arc<Calendar>>> {
|
||||
let (list, _ct) = CalendarList::load(user).await?;
|
||||
|
||||
match list.get(name) {
|
||||
None => Ok(None),
|
||||
Some(ident) => Ok(Some(self.open_by_id(user, ident).await?)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Open a calendar by unique id
|
||||
/// Check user.rs::open_mailbox_by_id to understand this function
|
||||
pub async fn open_by_id(&self, user: &Arc<User>, id: UniqueIdent) -> Result<Arc<Calendar>> {
|
||||
{
|
||||
let cache = self.0.lock().unwrap();
|
||||
if let Some(cal) = cache.get(&id).and_then(Weak::upgrade) {
|
||||
return Ok(cal);
|
||||
}
|
||||
}
|
||||
|
||||
let cal = Arc::new(Calendar::open(&user.creds, id).await?);
|
||||
|
||||
let mut cache = self.0.lock().unwrap();
|
||||
if let Some(concurrent_cal) = cache.get(&id).and_then(Weak::upgrade) {
|
||||
drop(cal); // we worked for nothing but at least we didn't starve someone else
|
||||
Ok(concurrent_cal)
|
||||
} else {
|
||||
cache.insert(id, Arc::downgrade(&cal));
|
||||
Ok(cal)
|
||||
}
|
||||
}
|
||||
|
||||
/// List calendars
|
||||
pub async fn list(&self, user: &Arc<User>) -> Result<Vec<String>> {
|
||||
CalendarList::load(user).await.map(|(list, _)| list.names())
|
||||
}
|
||||
|
||||
/// Delete a calendar from the index
|
||||
pub async fn delete(&self, user: &Arc<User>, name: &str) -> Result<()> {
|
||||
// We currently assume that main cal is a bit specific
|
||||
if name == MAIN_CAL {
|
||||
bail!("Cannot delete main calendar");
|
||||
}
|
||||
|
||||
let (mut list, ct) = CalendarList::load(user).await?;
|
||||
if list.has(name) {
|
||||
//@TODO: actually delete calendar content
|
||||
list.bind(name, None);
|
||||
list.save(user, ct).await?;
|
||||
Ok(())
|
||||
} else {
|
||||
bail!("Calendar {} does not exist", name);
|
||||
}
|
||||
}
|
||||
|
||||
/// Rename a calendar in the index
|
||||
pub async fn rename(&self, user: &Arc<User>, old: &str, new: &str) -> Result<()> {
|
||||
if old == MAIN_CAL {
|
||||
bail!("Renaming main calendar is not supported currently");
|
||||
}
|
||||
if !new.chars().all(char::is_alphanumeric) {
|
||||
bail!("Unsupported characters in new calendar name, only alphanumeric characters are allowed currently");
|
||||
}
|
||||
if new.len() > MAX_CALNAME_CHARS {
|
||||
bail!("Calendar name can't contain more than 32 characters");
|
||||
}
|
||||
|
||||
let (mut list, ct) = CalendarList::load(user).await?;
|
||||
list.rename(old, new)?;
|
||||
list.save(user, ct).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create calendar
|
||||
pub async fn create(&self, user: &Arc<User>, name: &str) -> Result<()> {
|
||||
if name == MAIN_CAL {
|
||||
bail!("Main calendar is automatically created, can't create it manually");
|
||||
}
|
||||
if !name.chars().all(char::is_alphanumeric) {
|
||||
bail!("Unsupported characters in new calendar name, only alphanumeric characters are allowed");
|
||||
}
|
||||
if name.len() > MAX_CALNAME_CHARS {
|
||||
bail!("Calendar name can't contain more than 32 characters");
|
||||
}
|
||||
|
||||
let (mut list, ct) = CalendarList::load(user).await?;
|
||||
match list.create(name) {
|
||||
CalendarExists::Existed(_) => bail!("Calendar {} already exists", name),
|
||||
CalendarExists::Created(_) => (),
|
||||
}
|
||||
list.save(user, ct).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Has calendar
|
||||
pub async fn has(&self, user: &Arc<User>, name: &str) -> Result<bool> {
|
||||
CalendarList::load(user).await.map(|(list, _)| list.has(name))
|
||||
}
|
||||
}
|
||||
|
||||
// ------
|
||||
// ------ From this point, implementation is hidden from the rest of the crate
|
||||
// ------
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct CalendarList(BTreeMap<String, CalendarListEntry>);
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Copy, Debug)]
|
||||
struct CalendarListEntry {
|
||||
id_lww: (u64, Option<UniqueIdent>),
|
||||
}
|
||||
|
||||
impl CalendarList {
|
||||
// ---- Index persistence related functions
|
||||
|
||||
/// Load from storage
|
||||
async fn load(user: &Arc<User>) -> Result<(Self, Option<storage::RowRef>)> {
|
||||
let row_ref = storage::RowRef::new(CAL_LIST_PK, CAL_LIST_SK);
|
||||
let (mut list, row) = match user
|
||||
.storage
|
||||
.row_fetch(&storage::Selector::Single(&row_ref))
|
||||
.await
|
||||
{
|
||||
Err(storage::StorageError::NotFound) => (Self::new(), None),
|
||||
Err(e) => return Err(e.into()),
|
||||
Ok(rv) => {
|
||||
let mut list = Self::new();
|
||||
let (row_ref, row_vals) = match rv.into_iter().next() {
|
||||
Some(row_val) => (row_val.row_ref, row_val.value),
|
||||
None => (row_ref, vec![]),
|
||||
};
|
||||
|
||||
for v in row_vals {
|
||||
if let storage::Alternative::Value(vbytes) = v {
|
||||
let list2 = open_deserialize::<CalendarList>(&vbytes, &user.creds.keys.master)?;
|
||||
list.merge(list2);
|
||||
}
|
||||
}
|
||||
(list, Some(row_ref))
|
||||
}
|
||||
};
|
||||
|
||||
// Create default calendars (currently only one calendar is created)
|
||||
let is_default_cal_missing = [MAIN_CAL]
|
||||
.iter()
|
||||
.map(|calname| list.create(calname))
|
||||
.fold(false, |acc, r| {
|
||||
acc || matches!(r, CalendarExists::Created(..))
|
||||
});
|
||||
|
||||
// Save the index if we created a new calendar
|
||||
if is_default_cal_missing {
|
||||
list.save(user, row.clone()).await?;
|
||||
}
|
||||
|
||||
Ok((list, row))
|
||||
}
|
||||
|
||||
/// Save an updated index
|
||||
async fn save(&self, user: &Arc<User>, ct: Option<storage::RowRef>) -> Result<()> {
|
||||
let list_blob = seal_serialize(self, &user.creds.keys.master)?;
|
||||
let rref = ct.unwrap_or(storage::RowRef::new(CAL_LIST_PK, CAL_LIST_SK));
|
||||
let row_val = storage::RowVal::new(rref, list_blob);
|
||||
user.storage.row_insert(vec![row_val]).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ----- Index manipulation functions
|
||||
|
||||
/// Ensure that a given calendar exists
|
||||
/// (Don't forget to save if it returns CalendarExists::Created)
|
||||
fn create(&mut self, name: &str) -> CalendarExists {
|
||||
if let Some(CalendarListEntry {
|
||||
id_lww: (_, Some(id))
|
||||
}) = self.0.get(name)
|
||||
{
|
||||
return CalendarExists::Existed(*id);
|
||||
}
|
||||
|
||||
let id = gen_ident();
|
||||
self.bind(name, Some(id)).unwrap();
|
||||
CalendarExists::Created(id)
|
||||
}
|
||||
|
||||
/// Get a list of all calendar names
|
||||
fn names(&self) -> Vec<String> {
|
||||
self.0
|
||||
.iter()
|
||||
.filter(|(_, v)| v.id_lww.1.is_some())
|
||||
.map(|(k, _)| k.to_string())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// For a given calendar name, get its Unique Identifier
|
||||
fn get(&self, name: &str) -> Option<UniqueIdent> {
|
||||
self.0.get(name).map(|CalendarListEntry {
|
||||
id_lww: (_, ident),
|
||||
}| *ident).flatten()
|
||||
}
|
||||
|
||||
/// Check if a given calendar name exists
|
||||
fn has(&self, name: &str) -> bool {
|
||||
self.get(name).is_some()
|
||||
}
|
||||
|
||||
/// Rename a calendar
|
||||
fn rename(&mut self, old: &str, new: &str) -> Result<()> {
|
||||
if self.has(new) {
|
||||
bail!("Calendar {} already exists", new);
|
||||
}
|
||||
let ident = match self.get(old) {
|
||||
None => bail!("Calendar {} does not exist", old),
|
||||
Some(ident) => ident,
|
||||
};
|
||||
|
||||
self.bind(old, None);
|
||||
self.bind(new, Some(ident));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ----- Internal logic
|
||||
|
||||
/// New is not publicly exposed, use `load` instead
|
||||
fn new() -> Self {
|
||||
Self(BTreeMap::new())
|
||||
}
|
||||
|
||||
/// Low level index updating logic (used to add/rename/delete) an entry
|
||||
fn bind(&mut self, name: &str, id: Option<UniqueIdent>) -> Option<()> {
|
||||
let (ts, id) = match self.0.get_mut(name) {
|
||||
None => {
|
||||
if id.is_none() {
|
||||
// User wants to delete entry with given name (passed id is None)
|
||||
// Entry does not exist (get_mut is None)
|
||||
// Nothing to do
|
||||
return None;
|
||||
} else {
|
||||
// User wants entry with given name to be present (id is Some)
|
||||
// Entry does not exist
|
||||
// Initialize entry
|
||||
(now_msec(), id)
|
||||
}
|
||||
}
|
||||
Some(CalendarListEntry {
|
||||
id_lww,
|
||||
}) => {
|
||||
if id_lww.1 == id {
|
||||
// Entry is already equals to the requested id (Option<UniqueIdent)
|
||||
// Nothing to do
|
||||
return None;
|
||||
} else {
|
||||
// Entry does not equal to what we know internally
|
||||
// We update the Last Write Win CRDT here with the new id value
|
||||
(
|
||||
std::cmp::max(id_lww.0 + 1, now_msec()),
|
||||
id,
|
||||
)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// If we did not return here, that's because we have to update
|
||||
// something in our internal index.
|
||||
self.0.insert(
|
||||
name.into(),
|
||||
CalendarListEntry { id_lww: (ts, id) },
|
||||
);
|
||||
Some(())
|
||||
}
|
||||
|
||||
// Merge 2 calendar lists by applying a LWW logic on each element
|
||||
fn merge(&mut self, list2: Self) {
|
||||
for (k, v) in list2.0.into_iter() {
|
||||
if let Some(e) = self.0.get_mut(&k) {
|
||||
e.merge(&v);
|
||||
} else {
|
||||
self.0.insert(k, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CalendarListEntry {
|
||||
fn merge(&mut self, other: &Self) {
|
||||
// Simple CRDT merge rule
|
||||
if other.id_lww.0 > self.id_lww.0
|
||||
|| (other.id_lww.0 == self.id_lww.0 && other.id_lww.1 > self.id_lww.1)
|
||||
{
|
||||
self.id_lww = other.id_lww;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum CalendarExists {
|
||||
Created(UniqueIdent),
|
||||
Existed(UniqueIdent),
|
||||
}
|
|
@ -0,0 +1,334 @@
|
|||
use anyhow::{bail, Result};
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use im::{OrdMap, OrdSet, ordset};
|
||||
|
||||
use aero_bayou::*;
|
||||
|
||||
use crate::unique_ident::{gen_ident, UniqueIdent};
|
||||
|
||||
/// Parents are only persisted in the event log,
|
||||
/// not in the checkpoints.
|
||||
pub type Token = UniqueIdent;
|
||||
pub type Parents = Vec<Token>;
|
||||
pub type SyncDesc = (Parents, Token);
|
||||
|
||||
pub type BlobId = UniqueIdent;
|
||||
pub type Etag = String;
|
||||
pub type FileName = String;
|
||||
pub type IndexEntry = (BlobId, FileName, Etag);
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct DavDag {
|
||||
/// Source of trust
|
||||
pub table: OrdMap<BlobId, IndexEntry>,
|
||||
|
||||
/// Indexes optimized for queries
|
||||
pub idx_by_filename: OrdMap<FileName, BlobId>,
|
||||
|
||||
// ------------ Below this line, data is ephemeral, ie. not checkpointed
|
||||
|
||||
/// Partial synchronization graph
|
||||
pub ancestors: OrdMap<Token, OrdSet<Token>>,
|
||||
|
||||
/// All nodes
|
||||
pub all_nodes: OrdSet<Token>,
|
||||
/// Head nodes
|
||||
pub heads: OrdSet<Token>,
|
||||
/// Origin nodes
|
||||
pub origins: OrdSet<Token>,
|
||||
|
||||
/// File change token by token
|
||||
pub change: OrdMap<Token, SyncChange>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum SyncChange {
|
||||
Ok(FileName),
|
||||
NotFound(FileName),
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
pub enum DavDagOp {
|
||||
/// Merge is a virtual operation run when multiple heads are discovered
|
||||
Merge(SyncDesc),
|
||||
|
||||
/// Add an item to the collection
|
||||
Put(SyncDesc, IndexEntry),
|
||||
|
||||
/// Delete an item from the collection
|
||||
Delete(SyncDesc, BlobId),
|
||||
}
|
||||
impl DavDagOp {
|
||||
pub fn token(&self) -> Token {
|
||||
match self {
|
||||
Self::Merge((_, t)) => *t,
|
||||
Self::Put((_, t), _) => *t,
|
||||
Self::Delete((_, t), _) => *t,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DavDag {
|
||||
pub fn op_merge(&self) -> DavDagOp {
|
||||
DavDagOp::Merge(self.sync_desc())
|
||||
}
|
||||
|
||||
pub fn op_put(&self, entry: IndexEntry) -> DavDagOp {
|
||||
DavDagOp::Put(self.sync_desc(), entry)
|
||||
}
|
||||
|
||||
pub fn op_delete(&self, blob_id: BlobId) -> DavDagOp {
|
||||
DavDagOp::Delete(self.sync_desc(), blob_id)
|
||||
}
|
||||
|
||||
// HELPER functions
|
||||
|
||||
pub fn heads_vec(&self) -> Vec<Token> {
|
||||
self.heads.clone().into_iter().collect()
|
||||
}
|
||||
|
||||
/// A sync descriptor
|
||||
pub fn sync_desc(&self) -> SyncDesc {
|
||||
(self.heads_vec(), gen_ident())
|
||||
}
|
||||
|
||||
/// Resolve a sync token
|
||||
pub fn resolve(&self, known: Token) -> Result<OrdSet<Token>> {
|
||||
let already_known = self.all_ancestors(known);
|
||||
|
||||
// We can't capture all missing events if we are not connected
|
||||
// to all sinks of the graph,
|
||||
// ie. if we don't already know all the sinks,
|
||||
// ie. if we are missing so much history that
|
||||
// the event log has been transformed into a checkpoint
|
||||
if !self.origins.is_subset(already_known.clone()) {
|
||||
bail!("Not enough history to produce a correct diff, a full resync is needed");
|
||||
}
|
||||
|
||||
// Missing items are *all existing graph items* from which
|
||||
// we removed *all items known by the given node*.
|
||||
// In other words, all values in `all_nodes` that are not in `already_known`.
|
||||
Ok(self.all_nodes.clone().relative_complement(already_known))
|
||||
}
|
||||
|
||||
/// Find all ancestors of a given node
|
||||
fn all_ancestors(&self, known: Token) -> OrdSet<Token> {
|
||||
let mut all_known: OrdSet<UniqueIdent> = OrdSet::new();
|
||||
let mut to_collect = vec![known];
|
||||
loop {
|
||||
let cursor = match to_collect.pop() {
|
||||
// Loop stops here
|
||||
None => break,
|
||||
Some(v) => v,
|
||||
};
|
||||
|
||||
if all_known.insert(cursor).is_some() {
|
||||
// Item already processed
|
||||
continue
|
||||
}
|
||||
|
||||
// Collect parents
|
||||
let parents = match self.ancestors.get(&cursor) {
|
||||
None => continue,
|
||||
Some(c) => c,
|
||||
};
|
||||
to_collect.extend(parents.iter());
|
||||
}
|
||||
all_known
|
||||
}
|
||||
|
||||
// INTERNAL functions
|
||||
|
||||
/// Register a WebDAV item (put, copy, move)
|
||||
fn register(&mut self, sync_token: Option<Token>, entry: IndexEntry) {
|
||||
let (blob_id, filename, _etag) = entry.clone();
|
||||
|
||||
// Insert item in the source of trust
|
||||
self.table.insert(blob_id, entry);
|
||||
|
||||
// Update the cache
|
||||
self.idx_by_filename.insert(filename.to_string(), blob_id);
|
||||
|
||||
// Record the change in the ephemeral synchronization map
|
||||
if let Some(sync_token) = sync_token {
|
||||
self.change.insert(sync_token, SyncChange::Ok(filename));
|
||||
}
|
||||
}
|
||||
|
||||
/// Unregister a WebDAV item (delete, move)
|
||||
fn unregister(&mut self, sync_token: Token, blob_id: &BlobId) {
|
||||
// Query the source of truth to get the information we
|
||||
// need to clean the indexes
|
||||
let (_blob_id, filename, _etag) = match self.table.get(blob_id) {
|
||||
Some(v) => v,
|
||||
// Element does not exist, return early
|
||||
None => return,
|
||||
};
|
||||
self.idx_by_filename.remove(filename);
|
||||
|
||||
// Record the change in the ephemeral synchronization map
|
||||
self.change.insert(sync_token, SyncChange::NotFound(filename.to_string()));
|
||||
|
||||
// Finally clear item from the source of trust
|
||||
self.table.remove(blob_id);
|
||||
}
|
||||
|
||||
/// When an event is processed, update the synchronization DAG
|
||||
fn sync_dag(&mut self, sync_desc: &SyncDesc) {
|
||||
let (parents, child) = sync_desc;
|
||||
|
||||
// --- Update ANCESTORS
|
||||
// We register ancestors as it is required for the sync algorithm
|
||||
self.ancestors.insert(*child, parents.iter().fold(ordset![], |mut acc, p| {
|
||||
acc.insert(*p);
|
||||
acc
|
||||
}));
|
||||
|
||||
// --- Update ORIGINS
|
||||
// If this event has no parents, it's an origin
|
||||
if parents.is_empty() {
|
||||
self.origins.insert(*child);
|
||||
}
|
||||
|
||||
// --- Update HEADS
|
||||
// Remove from HEADS this event's parents
|
||||
parents.iter().for_each(|par| { self.heads.remove(par); });
|
||||
|
||||
// This event becomes a new HEAD in turn
|
||||
self.heads.insert(*child);
|
||||
|
||||
// --- Update ALL NODES
|
||||
self.all_nodes.insert(*child);
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for DavDag {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str("DavDag\n")?;
|
||||
for elem in self.table.iter() {
|
||||
f.write_fmt(format_args!("\t{:?} => {:?}", elem.0, elem.1))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl BayouState for DavDag {
|
||||
type Op = DavDagOp;
|
||||
|
||||
fn apply(&self, op: &Self::Op) -> Self {
|
||||
let mut new = self.clone();
|
||||
|
||||
match op {
|
||||
DavDagOp::Put(sync_desc, entry) => {
|
||||
new.sync_dag(sync_desc);
|
||||
new.register(Some(sync_desc.1), entry.clone());
|
||||
},
|
||||
DavDagOp::Delete(sync_desc, blob_id) => {
|
||||
new.sync_dag(sync_desc);
|
||||
new.unregister(sync_desc.1, blob_id);
|
||||
},
|
||||
DavDagOp::Merge(sync_desc) => {
|
||||
new.sync_dag(sync_desc);
|
||||
}
|
||||
}
|
||||
|
||||
new
|
||||
}
|
||||
}
|
||||
|
||||
// CUSTOM SERIALIZATION & DESERIALIZATION
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct DavDagSerializedRepr {
|
||||
items: Vec<IndexEntry>,
|
||||
heads: Vec<UniqueIdent>,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for DavDag {
|
||||
fn deserialize<D>(d: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let val: DavDagSerializedRepr = DavDagSerializedRepr::deserialize(d)?;
|
||||
let mut davdag = DavDag::default();
|
||||
|
||||
// Build the table + index
|
||||
val.items.into_iter().for_each(|entry| davdag.register(None, entry));
|
||||
|
||||
// Initialize the synchronization DAG with its roots
|
||||
val.heads.into_iter().for_each(|ident| {
|
||||
davdag.heads.insert(ident);
|
||||
davdag.origins.insert(ident);
|
||||
davdag.all_nodes.insert(ident);
|
||||
});
|
||||
|
||||
Ok(davdag)
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for DavDag {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
// Indexes are rebuilt on the fly, we serialize only the core database
|
||||
let items = self.table.iter().map(|(_, entry)| entry.clone()).collect();
|
||||
|
||||
// We keep only the head entries from the sync graph,
|
||||
// these entries will be used to initialize it back when deserializing
|
||||
let heads = self.heads_vec();
|
||||
|
||||
// Finale serialization object
|
||||
let val = DavDagSerializedRepr { items, heads };
|
||||
val.serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
// ---- TESTS ----
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn base() {
|
||||
let mut state = DavDag::default();
|
||||
|
||||
// Add item 1
|
||||
{
|
||||
let m = UniqueIdent([0x01; 24]);
|
||||
let ev = state.op_put((m, "cal.ics".into(), "321-321".into()));
|
||||
state = state.apply(&ev);
|
||||
|
||||
assert_eq!(state.table.len(), 1);
|
||||
assert_eq!(state.resolve(ev.token()).unwrap().len(), 0);
|
||||
}
|
||||
|
||||
// Add 2 concurrent items
|
||||
let (t1, t2) = {
|
||||
let blob1 = UniqueIdent([0x02; 24]);
|
||||
let ev1 = state.op_put((blob1, "cal2.ics".into(), "321-321".into()));
|
||||
|
||||
let blob2 = UniqueIdent([0x01; 24]);
|
||||
let ev2 = state.op_delete(blob2);
|
||||
|
||||
state = state.apply(&ev1);
|
||||
state = state.apply(&ev2);
|
||||
|
||||
assert_eq!(state.table.len(), 1);
|
||||
assert_eq!(state.resolve(ev1.token()).unwrap(), ordset![ev2.token()]);
|
||||
|
||||
(ev1.token(), ev2.token())
|
||||
};
|
||||
|
||||
// Add later a new item
|
||||
{
|
||||
let blob3 = UniqueIdent([0x03; 24]);
|
||||
let ev = state.op_put((blob3, "cal3.ics".into(), "321-321".into()));
|
||||
|
||||
state = state.apply(&ev);
|
||||
assert_eq!(state.table.len(), 2);
|
||||
assert_eq!(state.resolve(ev.token()).unwrap().len(), 0);
|
||||
assert_eq!(state.resolve(t1).unwrap(), ordset![t2, ev.token()]);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
pub mod unique_ident;
|
||||
pub mod davdag;
|
||||
pub mod user;
|
||||
pub mod mail;
|
||||
pub mod calendar;
|
|
@ -1,6 +1,3 @@
|
|||
//use std::collections::HashMap;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use std::sync::{Arc, Weak};
|
||||
use std::time::Duration;
|
||||
|
||||
|
@ -11,15 +8,16 @@ use futures::{future::BoxFuture, FutureExt};
|
|||
use tokio::sync::watch;
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
||||
use crate::cryptoblob;
|
||||
use crate::login::{Credentials, PublicCredentials};
|
||||
use aero_user::cryptoblob;
|
||||
use aero_user::login::{Credentials, PublicCredentials};
|
||||
use aero_user::storage;
|
||||
use aero_bayou::timestamp::now_msec;
|
||||
|
||||
use crate::mail::mailbox::Mailbox;
|
||||
use crate::mail::uidindex::ImapUidvalidity;
|
||||
use crate::mail::unique_ident::*;
|
||||
use crate::mail::user::User;
|
||||
use crate::unique_ident::*;
|
||||
use crate::user::User;
|
||||
use crate::mail::IMF;
|
||||
use crate::storage;
|
||||
use crate::timestamp::now_msec;
|
||||
|
||||
const INCOMING_PK: &str = "incoming";
|
||||
const INCOMING_LOCK_SK: &str = "lock";
|
|
@ -2,14 +2,15 @@ use anyhow::{anyhow, bail, Result};
|
|||
use serde::{Deserialize, Serialize};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::bayou::Bayou;
|
||||
use crate::cryptoblob::{self, gen_key, open_deserialize, seal_serialize, Key};
|
||||
use crate::login::Credentials;
|
||||
use aero_user::cryptoblob::{self, gen_key, open_deserialize, seal_serialize, Key};
|
||||
use aero_user::login::Credentials;
|
||||
use aero_user::storage::{self, BlobRef, BlobVal, RowRef, RowVal, Selector, Store};
|
||||
use aero_bayou::Bayou;
|
||||
use aero_bayou::timestamp::now_msec;
|
||||
|
||||
use crate::unique_ident::*;
|
||||
use crate::mail::uidindex::*;
|
||||
use crate::mail::unique_ident::*;
|
||||
use crate::mail::IMF;
|
||||
use crate::storage::{self, BlobRef, BlobVal, RowRef, RowVal, Selector, Store};
|
||||
use crate::timestamp::now_msec;
|
||||
|
||||
pub struct Mailbox {
|
||||
pub(super) id: UniqueIdent,
|
||||
|
@ -17,7 +18,7 @@ pub struct Mailbox {
|
|||
}
|
||||
|
||||
impl Mailbox {
|
||||
pub(super) async fn open(
|
||||
pub(crate) async fn open(
|
||||
creds: &Credentials,
|
||||
id: UniqueIdent,
|
||||
min_uidvalidity: ImapUidvalidity,
|
||||
|
@ -374,7 +375,7 @@ impl MailboxInternal {
|
|||
|
||||
async fn delete(&mut self, ident: UniqueIdent) -> Result<()> {
|
||||
if !self.uid_index.state().table.contains_key(&ident) {
|
||||
bail!("Cannot delete mail that doesn't exit");
|
||||
bail!("Cannot delete mail that doesn't exist");
|
||||
}
|
||||
|
||||
let del_mail_op = self.uid_index.state().op_mail_del(ident);
|
|
@ -1,12 +1,9 @@
|
|||
use std::convert::TryFrom;
|
||||
|
||||
pub mod incoming;
|
||||
pub mod mailbox;
|
||||
pub mod query;
|
||||
pub mod snapshot;
|
||||
pub mod uidindex;
|
||||
pub mod unique_ident;
|
||||
pub mod user;
|
||||
pub mod namespace;
|
||||
|
||||
// Internet Message Format
|
||||
// aka RFC 822 - RFC 2822 - RFC 5322
|
|
@ -0,0 +1,202 @@
|
|||
use std::collections::BTreeMap;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use aero_bayou::timestamp::now_msec;
|
||||
|
||||
use crate::mail::uidindex::ImapUidvalidity;
|
||||
use crate::unique_ident::{gen_ident, UniqueIdent};
|
||||
|
||||
pub const MAILBOX_HIERARCHY_DELIMITER: char = '.';
|
||||
|
||||
/// INBOX is the only mailbox that must always exist.
|
||||
/// It is created automatically when the account is created.
|
||||
/// IMAP allows the user to rename INBOX to something else,
|
||||
/// in this case all messages from INBOX are moved to a mailbox
|
||||
/// with the new name and the INBOX mailbox still exists and is empty.
|
||||
/// In our implementation, we indeed move the underlying mailbox
|
||||
/// to the new name (i.e. the new name has the same id as the previous
|
||||
/// INBOX), and we create a new empty mailbox for INBOX.
|
||||
pub const INBOX: &str = "INBOX";
|
||||
|
||||
/// For convenience purpose, we also create some special mailbox
|
||||
/// that are described in RFC6154 SPECIAL-USE
|
||||
/// @FIXME maybe it should be a configuration parameter
|
||||
/// @FIXME maybe we should have a per-mailbox flag mechanism, either an enum or a string, so we
|
||||
/// track which mailbox is used for what.
|
||||
/// @FIXME Junk could be useful but we don't have any antispam solution yet so...
|
||||
/// @FIXME IMAP supports virtual mailbox. \All or \Flagged are intended to be virtual mailboxes.
|
||||
/// \Trash might be one, or not one. I don't know what we should do there.
|
||||
pub const DRAFTS: &str = "Drafts";
|
||||
pub const ARCHIVE: &str = "Archive";
|
||||
pub const SENT: &str = "Sent";
|
||||
pub const TRASH: &str = "Trash";
|
||||
|
||||
pub(crate) const MAILBOX_LIST_PK: &str = "mailboxes";
|
||||
pub(crate) const MAILBOX_LIST_SK: &str = "list";
|
||||
|
||||
// ---- User's mailbox list (serialized in K2V) ----
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub(crate) struct MailboxList(BTreeMap<String, MailboxListEntry>);
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Copy, Debug)]
|
||||
pub(crate) struct MailboxListEntry {
|
||||
id_lww: (u64, Option<UniqueIdent>),
|
||||
uidvalidity: ImapUidvalidity,
|
||||
}
|
||||
|
||||
impl MailboxListEntry {
|
||||
fn merge(&mut self, other: &Self) {
|
||||
// Simple CRDT merge rule
|
||||
if other.id_lww.0 > self.id_lww.0
|
||||
|| (other.id_lww.0 == self.id_lww.0 && other.id_lww.1 > self.id_lww.1)
|
||||
{
|
||||
self.id_lww = other.id_lww;
|
||||
}
|
||||
self.uidvalidity = std::cmp::max(self.uidvalidity, other.uidvalidity);
|
||||
}
|
||||
}
|
||||
|
||||
impl MailboxList {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self(BTreeMap::new())
|
||||
}
|
||||
|
||||
pub(crate) fn merge(&mut self, list2: Self) {
|
||||
for (k, v) in list2.0.into_iter() {
|
||||
if let Some(e) = self.0.get_mut(&k) {
|
||||
e.merge(&v);
|
||||
} else {
|
||||
self.0.insert(k, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn existing_mailbox_names(&self) -> Vec<String> {
|
||||
self.0
|
||||
.iter()
|
||||
.filter(|(_, v)| v.id_lww.1.is_some())
|
||||
.map(|(k, _)| k.to_string())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn has_mailbox(&self, name: &str) -> bool {
|
||||
matches!(
|
||||
self.0.get(name),
|
||||
Some(MailboxListEntry {
|
||||
id_lww: (_, Some(_)),
|
||||
..
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn get_mailbox(&self, name: &str) -> Option<(ImapUidvalidity, Option<UniqueIdent>)> {
|
||||
self.0.get(name).map(
|
||||
|MailboxListEntry {
|
||||
id_lww: (_, mailbox_id),
|
||||
uidvalidity,
|
||||
}| (*uidvalidity, *mailbox_id),
|
||||
)
|
||||
}
|
||||
|
||||
/// Ensures mailbox `name` maps to id `id`.
|
||||
/// If it already mapped to that, returns None.
|
||||
/// If a change had to be done, returns Some(new uidvalidity in mailbox).
|
||||
pub(crate) fn set_mailbox(&mut self, name: &str, id: Option<UniqueIdent>) -> Option<ImapUidvalidity> {
|
||||
let (ts, id, uidvalidity) = match self.0.get_mut(name) {
|
||||
None => {
|
||||
if id.is_none() {
|
||||
return None;
|
||||
} else {
|
||||
(now_msec(), id, ImapUidvalidity::new(1).unwrap())
|
||||
}
|
||||
}
|
||||
Some(MailboxListEntry {
|
||||
id_lww,
|
||||
uidvalidity,
|
||||
}) => {
|
||||
if id_lww.1 == id {
|
||||
return None;
|
||||
} else {
|
||||
(
|
||||
std::cmp::max(id_lww.0 + 1, now_msec()),
|
||||
id,
|
||||
ImapUidvalidity::new(uidvalidity.get() + 1).unwrap(),
|
||||
)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
self.0.insert(
|
||||
name.into(),
|
||||
MailboxListEntry {
|
||||
id_lww: (ts, id),
|
||||
uidvalidity,
|
||||
},
|
||||
);
|
||||
Some(uidvalidity)
|
||||
}
|
||||
|
||||
pub(crate) fn update_uidvalidity(&mut self, name: &str, new_uidvalidity: ImapUidvalidity) {
|
||||
match self.0.get_mut(name) {
|
||||
None => {
|
||||
self.0.insert(
|
||||
name.into(),
|
||||
MailboxListEntry {
|
||||
id_lww: (now_msec(), None),
|
||||
uidvalidity: new_uidvalidity,
|
||||
},
|
||||
);
|
||||
}
|
||||
Some(MailboxListEntry { uidvalidity, .. }) => {
|
||||
*uidvalidity = std::cmp::max(*uidvalidity, new_uidvalidity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn create_mailbox(&mut self, name: &str) -> CreatedMailbox {
|
||||
if let Some(MailboxListEntry {
|
||||
id_lww: (_, Some(id)),
|
||||
uidvalidity,
|
||||
}) = self.0.get(name)
|
||||
{
|
||||
return CreatedMailbox::Existed(*id, *uidvalidity);
|
||||
}
|
||||
|
||||
let id = gen_ident();
|
||||
let uidvalidity = self.set_mailbox(name, Some(id)).unwrap();
|
||||
CreatedMailbox::Created(id, uidvalidity)
|
||||
}
|
||||
|
||||
pub(crate) fn rename_mailbox(&mut self, old_name: &str, new_name: &str) -> Result<()> {
|
||||
if let Some((uidvalidity, Some(mbid))) = self.get_mailbox(old_name) {
|
||||
if self.has_mailbox(new_name) {
|
||||
bail!(
|
||||
"Cannot rename {} into {}: {} already exists",
|
||||
old_name,
|
||||
new_name,
|
||||
new_name
|
||||
);
|
||||
}
|
||||
|
||||
self.set_mailbox(old_name, None);
|
||||
self.set_mailbox(new_name, Some(mbid));
|
||||
self.update_uidvalidity(new_name, uidvalidity);
|
||||
Ok(())
|
||||
} else {
|
||||
bail!(
|
||||
"Cannot rename {} into {}: {} doesn't exist",
|
||||
old_name,
|
||||
new_name,
|
||||
old_name
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum CreatedMailbox {
|
||||
Created(UniqueIdent, ImapUidvalidity),
|
||||
Existed(UniqueIdent, ImapUidvalidity),
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
use super::mailbox::MailMeta;
|
||||
use super::snapshot::FrozenMailbox;
|
||||
use super::unique_ident::UniqueIdent;
|
||||
use crate::unique_ident::UniqueIdent;
|
||||
use anyhow::Result;
|
||||
use futures::future::FutureExt;
|
||||
use futures::stream::{BoxStream, Stream, StreamExt};
|
|
@ -2,10 +2,10 @@ use std::sync::Arc;
|
|||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::unique_ident::UniqueIdent;
|
||||
use super::mailbox::Mailbox;
|
||||
use super::query::{Query, QueryScope};
|
||||
use super::uidindex::UidIndex;
|
||||
use super::unique_ident::UniqueIdent;
|
||||
|
||||
/// A Frozen Mailbox has a snapshot of the current mailbox
|
||||
/// state that is desynchronized with the real mailbox state.
|
|
@ -3,8 +3,8 @@ use std::num::{NonZeroU32, NonZeroU64};
|
|||
use im::{HashMap, OrdMap, OrdSet};
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
use crate::bayou::*;
|
||||
use crate::mail::unique_ident::UniqueIdent;
|
||||
use aero_bayou::*;
|
||||
use crate::unique_ident::UniqueIdent;
|
||||
|
||||
pub type ModSeq = NonZeroU64;
|
||||
pub type ImapUid = NonZeroU32;
|
|
@ -5,9 +5,9 @@ use lazy_static::lazy_static;
|
|||
use rand::prelude::*;
|
||||
use serde::{de::Error, Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
use crate::timestamp::now_msec;
|
||||
use aero_bayou::timestamp::now_msec;
|
||||
|
||||
/// An internal Mail Identifier is composed of two components:
|
||||
/// An internal Aerogramme identifier is composed of two components:
|
||||
/// - a process identifier, 128 bits, itself composed of:
|
||||
/// - the timestamp of when the process started, 64 bits
|
||||
/// - a 64-bit random number
|
||||
|
@ -15,7 +15,7 @@ use crate::timestamp::now_msec;
|
|||
/// They are not part of the protocol but an internal representation
|
||||
/// required by Aerogramme.
|
||||
/// Their main property is to be unique without having to rely
|
||||
/// on synchronization between IMAP processes.
|
||||
/// on synchronization between (IMAP) processes.
|
||||
#[derive(Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)]
|
||||
pub struct UniqueIdent(pub [u8; 24]);
|
||||
|
|
@ -1,54 +1,38 @@
|
|||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Weak};
|
||||
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use lazy_static::lazy_static;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::sync::watch;
|
||||
|
||||
use crate::cryptoblob::{open_deserialize, seal_serialize};
|
||||
use crate::login::Credentials;
|
||||
use aero_user::cryptoblob::{open_deserialize, seal_serialize};
|
||||
use aero_user::login::Credentials;
|
||||
use aero_user::storage;
|
||||
|
||||
use crate::mail::incoming::incoming_mail_watch_process;
|
||||
use crate::mail::mailbox::Mailbox;
|
||||
use crate::mail::uidindex::ImapUidvalidity;
|
||||
use crate::mail::unique_ident::{gen_ident, UniqueIdent};
|
||||
use crate::storage;
|
||||
use crate::timestamp::now_msec;
|
||||
use crate::unique_ident::UniqueIdent;
|
||||
use crate::mail::namespace::{MAILBOX_HIERARCHY_DELIMITER, INBOX, DRAFTS, ARCHIVE, SENT, TRASH, MAILBOX_LIST_PK, MAILBOX_LIST_SK,MailboxList,CreatedMailbox};
|
||||
use crate::calendar::namespace::CalendarNs;
|
||||
|
||||
pub const MAILBOX_HIERARCHY_DELIMITER: char = '.';
|
||||
//@FIXME User should be totally rewriten
|
||||
// to extract the local mailbox list
|
||||
// to the mail/namespace.rs file (and mailbox list should be reworded as mail namespace)
|
||||
|
||||
/// INBOX is the only mailbox that must always exist.
|
||||
/// It is created automatically when the account is created.
|
||||
/// IMAP allows the user to rename INBOX to something else,
|
||||
/// in this case all messages from INBOX are moved to a mailbox
|
||||
/// with the new name and the INBOX mailbox still exists and is empty.
|
||||
/// In our implementation, we indeed move the underlying mailbox
|
||||
/// to the new name (i.e. the new name has the same id as the previous
|
||||
/// INBOX), and we create a new empty mailbox for INBOX.
|
||||
pub const INBOX: &str = "INBOX";
|
||||
|
||||
/// For convenience purpose, we also create some special mailbox
|
||||
/// that are described in RFC6154 SPECIAL-USE
|
||||
/// @FIXME maybe it should be a configuration parameter
|
||||
/// @FIXME maybe we should have a per-mailbox flag mechanism, either an enum or a string, so we
|
||||
/// track which mailbox is used for what.
|
||||
/// @FIXME Junk could be useful but we don't have any antispam solution yet so...
|
||||
/// @FIXME IMAP supports virtual mailbox. \All or \Flagged are intended to be virtual mailboxes.
|
||||
/// \Trash might be one, or not one. I don't know what we should do there.
|
||||
pub const DRAFTS: &str = "Drafts";
|
||||
pub const ARCHIVE: &str = "Archive";
|
||||
pub const SENT: &str = "Sent";
|
||||
pub const TRASH: &str = "Trash";
|
||||
|
||||
const MAILBOX_LIST_PK: &str = "mailboxes";
|
||||
const MAILBOX_LIST_SK: &str = "list";
|
||||
//@FIXME User should be run in a LocalSet
|
||||
// to remove most - if not all - synchronizations types.
|
||||
// Especially RwLock & co.
|
||||
|
||||
pub struct User {
|
||||
pub username: String,
|
||||
pub creds: Credentials,
|
||||
pub storage: storage::Store,
|
||||
pub mailboxes: std::sync::Mutex<HashMap<UniqueIdent, Weak<Mailbox>>>,
|
||||
pub calendars: CalendarNs,
|
||||
|
||||
// Handle on worker processing received email
|
||||
// (moving emails from the mailqueue to the user's INBOX)
|
||||
tx_inbox_id: watch::Sender<Option<(UniqueIdent, ImapUidvalidity)>>,
|
||||
}
|
||||
|
||||
|
@ -202,6 +186,7 @@ impl User {
|
|||
storage,
|
||||
tx_inbox_id,
|
||||
mailboxes: std::sync::Mutex::new(HashMap::new()),
|
||||
calendars: CalendarNs::new(),
|
||||
});
|
||||
|
||||
// Ensure INBOX exists (done inside load_mailbox_list)
|
||||
|
@ -228,6 +213,10 @@ impl User {
|
|||
}
|
||||
}
|
||||
|
||||
// The idea here is that:
|
||||
// 1. Opening a mailbox that is not already opened takes a significant amount of time
|
||||
// 2. We don't want to lock the whole HashMap that contain the mailboxes during this
|
||||
// operation which is why we droppped the lock above but take it again below.
|
||||
let mb = Arc::new(Mailbox::open(&self.creds, id, min_uidvalidity).await?);
|
||||
|
||||
let mut cache = self.mailboxes.lock().unwrap();
|
||||
|
@ -327,171 +316,6 @@ impl User {
|
|||
}
|
||||
}
|
||||
|
||||
// ---- User's mailbox list (serialized in K2V) ----
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct MailboxList(BTreeMap<String, MailboxListEntry>);
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Copy, Debug)]
|
||||
struct MailboxListEntry {
|
||||
id_lww: (u64, Option<UniqueIdent>),
|
||||
uidvalidity: ImapUidvalidity,
|
||||
}
|
||||
|
||||
impl MailboxListEntry {
|
||||
fn merge(&mut self, other: &Self) {
|
||||
// Simple CRDT merge rule
|
||||
if other.id_lww.0 > self.id_lww.0
|
||||
|| (other.id_lww.0 == self.id_lww.0 && other.id_lww.1 > self.id_lww.1)
|
||||
{
|
||||
self.id_lww = other.id_lww;
|
||||
}
|
||||
self.uidvalidity = std::cmp::max(self.uidvalidity, other.uidvalidity);
|
||||
}
|
||||
}
|
||||
|
||||
impl MailboxList {
|
||||
fn new() -> Self {
|
||||
Self(BTreeMap::new())
|
||||
}
|
||||
|
||||
fn merge(&mut self, list2: Self) {
|
||||
for (k, v) in list2.0.into_iter() {
|
||||
if let Some(e) = self.0.get_mut(&k) {
|
||||
e.merge(&v);
|
||||
} else {
|
||||
self.0.insert(k, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn existing_mailbox_names(&self) -> Vec<String> {
|
||||
self.0
|
||||
.iter()
|
||||
.filter(|(_, v)| v.id_lww.1.is_some())
|
||||
.map(|(k, _)| k.to_string())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn has_mailbox(&self, name: &str) -> bool {
|
||||
matches!(
|
||||
self.0.get(name),
|
||||
Some(MailboxListEntry {
|
||||
id_lww: (_, Some(_)),
|
||||
..
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
fn get_mailbox(&self, name: &str) -> Option<(ImapUidvalidity, Option<UniqueIdent>)> {
|
||||
self.0.get(name).map(
|
||||
|MailboxListEntry {
|
||||
id_lww: (_, mailbox_id),
|
||||
uidvalidity,
|
||||
}| (*uidvalidity, *mailbox_id),
|
||||
)
|
||||
}
|
||||
|
||||
/// Ensures mailbox `name` maps to id `id`.
|
||||
/// If it already mapped to that, returns None.
|
||||
/// If a change had to be done, returns Some(new uidvalidity in mailbox).
|
||||
fn set_mailbox(&mut self, name: &str, id: Option<UniqueIdent>) -> Option<ImapUidvalidity> {
|
||||
let (ts, id, uidvalidity) = match self.0.get_mut(name) {
|
||||
None => {
|
||||
if id.is_none() {
|
||||
return None;
|
||||
} else {
|
||||
(now_msec(), id, ImapUidvalidity::new(1).unwrap())
|
||||
}
|
||||
}
|
||||
Some(MailboxListEntry {
|
||||
id_lww,
|
||||
uidvalidity,
|
||||
}) => {
|
||||
if id_lww.1 == id {
|
||||
return None;
|
||||
} else {
|
||||
(
|
||||
std::cmp::max(id_lww.0 + 1, now_msec()),
|
||||
id,
|
||||
ImapUidvalidity::new(uidvalidity.get() + 1).unwrap(),
|
||||
)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
self.0.insert(
|
||||
name.into(),
|
||||
MailboxListEntry {
|
||||
id_lww: (ts, id),
|
||||
uidvalidity,
|
||||
},
|
||||
);
|
||||
Some(uidvalidity)
|
||||
}
|
||||
|
||||
fn update_uidvalidity(&mut self, name: &str, new_uidvalidity: ImapUidvalidity) {
|
||||
match self.0.get_mut(name) {
|
||||
None => {
|
||||
self.0.insert(
|
||||
name.into(),
|
||||
MailboxListEntry {
|
||||
id_lww: (now_msec(), None),
|
||||
uidvalidity: new_uidvalidity,
|
||||
},
|
||||
);
|
||||
}
|
||||
Some(MailboxListEntry { uidvalidity, .. }) => {
|
||||
*uidvalidity = std::cmp::max(*uidvalidity, new_uidvalidity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn create_mailbox(&mut self, name: &str) -> CreatedMailbox {
|
||||
if let Some(MailboxListEntry {
|
||||
id_lww: (_, Some(id)),
|
||||
uidvalidity,
|
||||
}) = self.0.get(name)
|
||||
{
|
||||
return CreatedMailbox::Existed(*id, *uidvalidity);
|
||||
}
|
||||
|
||||
let id = gen_ident();
|
||||
let uidvalidity = self.set_mailbox(name, Some(id)).unwrap();
|
||||
CreatedMailbox::Created(id, uidvalidity)
|
||||
}
|
||||
|
||||
fn rename_mailbox(&mut self, old_name: &str, new_name: &str) -> Result<()> {
|
||||
if let Some((uidvalidity, Some(mbid))) = self.get_mailbox(old_name) {
|
||||
if self.has_mailbox(new_name) {
|
||||
bail!(
|
||||
"Cannot rename {} into {}: {} already exists",
|
||||
old_name,
|
||||
new_name,
|
||||
new_name
|
||||
);
|
||||
}
|
||||
|
||||
self.set_mailbox(old_name, None);
|
||||
self.set_mailbox(new_name, Some(mbid));
|
||||
self.update_uidvalidity(new_name, uidvalidity);
|
||||
Ok(())
|
||||
} else {
|
||||
bail!(
|
||||
"Cannot rename {} into {}: {} doesn't exist",
|
||||
old_name,
|
||||
new_name,
|
||||
old_name
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum CreatedMailbox {
|
||||
Created(UniqueIdent, ImapUidvalidity),
|
||||
Existed(UniqueIdent, ImapUidvalidity),
|
||||
}
|
||||
|
||||
// ---- User cache ----
|
||||
|
||||
lazy_static! {
|
|
@ -0,0 +1 @@
|
|||
target/
|
|
@ -0,0 +1,14 @@
|
|||
[package]
|
||||
name = "aero-dav"
|
||||
version = "0.3.0"
|
||||
authors = ["Alex Auvolat <alex@adnab.me>", "Quentin Dufour <quentin@dufour.io>"]
|
||||
edition = "2021"
|
||||
license = "EUPL-1.2"
|
||||
description = "A partial and standalone implementation of the WebDAV protocol and its extensions (eg. CalDAV or CardDAV)"
|
||||
|
||||
[dependencies]
|
||||
quick-xml.workspace = true
|
||||
http.workspace = true
|
||||
chrono.workspace = true
|
||||
tokio.workspace = true
|
||||
futures.workspace = true
|
|
@ -0,0 +1,4 @@
|
|||
target
|
||||
corpus
|
||||
artifacts
|
||||
coverage
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,24 @@
|
|||
[package]
|
||||
name = "aerogramme-fuzz"
|
||||
version = "0.0.0"
|
||||
publish = false
|
||||
edition = "2021"
|
||||
|
||||
[package.metadata]
|
||||
cargo-fuzz = true
|
||||
|
||||
[dependencies]
|
||||
arbitrary = { version = "1", optional = true, features = ["derive"] }
|
||||
libfuzzer-sys = { version = "0.4", features = ["arbitrary-derive"] }
|
||||
tokio = { version = "1.18", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "time", "macros", "sync", "signal", "fs"] }
|
||||
quick-xml = { version = "0.31", features = ["async-tokio"] }
|
||||
|
||||
[dependencies.aero-dav]
|
||||
path = ".."
|
||||
|
||||
[[bin]]
|
||||
name = "dav"
|
||||
path = "fuzz_targets/dav.rs"
|
||||
test = false
|
||||
doc = false
|
||||
bench = false
|
|
@ -0,0 +1,126 @@
|
|||
#
|
||||
# AFL dictionary for XML
|
||||
# ----------------------
|
||||
#
|
||||
# Several basic syntax elements and attributes, modeled on libxml2.
|
||||
#
|
||||
# Created by Michal Zalewski
|
||||
#
|
||||
|
||||
attr_encoding=" encoding=\"1\""
|
||||
attr_generic=" a=\"1\""
|
||||
attr_href=" href=\"1\""
|
||||
attr_standalone=" standalone=\"no\""
|
||||
attr_version=" version=\"1\""
|
||||
attr_xml_base=" xml:base=\"1\""
|
||||
attr_xml_id=" xml:id=\"1\""
|
||||
attr_xml_lang=" xml:lang=\"1\""
|
||||
attr_xml_space=" xml:space=\"1\""
|
||||
attr_xmlns=" xmlns=\"1\""
|
||||
|
||||
entity_builtin="<"
|
||||
entity_decimal=""
|
||||
entity_external="&a;"
|
||||
entity_hex=""
|
||||
|
||||
string_any="ANY"
|
||||
string_brackets="[]"
|
||||
string_cdata="CDATA"
|
||||
string_col_fallback=":fallback"
|
||||
string_col_generic=":a"
|
||||
string_col_include=":include"
|
||||
string_dashes="--"
|
||||
string_empty="EMPTY"
|
||||
string_empty_dblquotes="\"\""
|
||||
string_empty_quotes="''"
|
||||
string_entities="ENTITIES"
|
||||
string_entity="ENTITY"
|
||||
string_fixed="#FIXED"
|
||||
string_id="ID"
|
||||
string_idref="IDREF"
|
||||
string_idrefs="IDREFS"
|
||||
string_implied="#IMPLIED"
|
||||
string_nmtoken="NMTOKEN"
|
||||
string_nmtokens="NMTOKENS"
|
||||
string_notation="NOTATION"
|
||||
string_parentheses="()"
|
||||
string_pcdata="#PCDATA"
|
||||
string_percent="%a"
|
||||
string_public="PUBLIC"
|
||||
string_required="#REQUIRED"
|
||||
string_schema=":schema"
|
||||
string_system="SYSTEM"
|
||||
string_ucs4="UCS-4"
|
||||
string_utf16="UTF-16"
|
||||
string_utf8="UTF-8"
|
||||
string_xmlns="xmlns:"
|
||||
|
||||
tag_attlist="<!ATTLIST"
|
||||
tag_cdata="<![CDATA["
|
||||
tag_close="</a>"
|
||||
tag_doctype="<!DOCTYPE"
|
||||
tag_element="<!ELEMENT"
|
||||
tag_entity="<!ENTITY"
|
||||
tag_ignore="<![IGNORE["
|
||||
tag_include="<![INCLUDE["
|
||||
tag_notation="<!NOTATION"
|
||||
tag_open="<a>"
|
||||
tag_open_close="<a />"
|
||||
tag_open_exclamation="<!"
|
||||
tag_open_q="<?"
|
||||
tag_sq2_close="]]>"
|
||||
tag_xml_q="<?xml?>"
|
||||
|
||||
"0"
|
||||
"1"
|
||||
"activelock"
|
||||
"allprop"
|
||||
"cannot-modify-protected-property"
|
||||
"collection"
|
||||
"creationdate"
|
||||
"DAV:"
|
||||
"depth"
|
||||
"displayname"
|
||||
"error"
|
||||
"exclusive"
|
||||
"getcontentlanguage"
|
||||
"getcontentlength"
|
||||
"getcontenttype"
|
||||
"getetag"
|
||||
"getlastmodified"
|
||||
"href"
|
||||
"include"
|
||||
"Infinite"
|
||||
"infinity"
|
||||
"location"
|
||||
"lockdiscovery"
|
||||
"lockentry"
|
||||
"lockinfo"
|
||||
"lockroot"
|
||||
"lockscope"
|
||||
"locktoken"
|
||||
"lock-token-matches-request-uri"
|
||||
"lock-token-submitted"
|
||||
"locktype"
|
||||
"multistatus"
|
||||
"no-conflicting-lock"
|
||||
"no-external-entities"
|
||||
"owner"
|
||||
"preserved-live-properties"
|
||||
"prop"
|
||||
"propertyupdate"
|
||||
"propfind"
|
||||
"propfind-finite-depth"
|
||||
"propname"
|
||||
"propstat"
|
||||
"remove"
|
||||
"resourcetype"
|
||||
"response"
|
||||
"responsedescription"
|
||||
"set"
|
||||
"shared"
|
||||
"status"
|
||||
"supportedlock"
|
||||
"text/html"
|
||||
"timeout"
|
||||
"write"
|
|
@ -0,0 +1,200 @@
|
|||
#![no_main]
|
||||
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
use libfuzzer_sys::arbitrary;
|
||||
use libfuzzer_sys::arbitrary::Arbitrary;
|
||||
|
||||
use aero_dav::{types, realization, xml};
|
||||
use quick_xml::reader::NsReader;
|
||||
use tokio::runtime::Runtime;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
// Split this file
|
||||
const tokens: [&str; 63] = [
|
||||
"0",
|
||||
"1",
|
||||
"activelock",
|
||||
"allprop",
|
||||
"encoding",
|
||||
"utf-8",
|
||||
"http://ns.example.com/boxschema/",
|
||||
"HTTP/1.1 200 OK",
|
||||
"1997-12-01T18:27:21-08:00",
|
||||
"Mon, 12 Jan 1998 09:25:56 GMT",
|
||||
"\"abcdef\"",
|
||||
"cannot-modify-protected-property",
|
||||
"collection",
|
||||
"creationdate",
|
||||
"DAV:",
|
||||
"D",
|
||||
"C",
|
||||
"xmlns:D",
|
||||
"depth",
|
||||
"displayname",
|
||||
"error",
|
||||
"exclusive",
|
||||
"getcontentlanguage",
|
||||
"getcontentlength",
|
||||
"getcontenttype",
|
||||
"getetag",
|
||||
"getlastmodified",
|
||||
"href",
|
||||
"include",
|
||||
"Infinite",
|
||||
"infinity",
|
||||
"location",
|
||||
"lockdiscovery",
|
||||
"lockentry",
|
||||
"lockinfo",
|
||||
"lockroot",
|
||||
"lockscope",
|
||||
"locktoken",
|
||||
"lock-token-matches-request-uri",
|
||||
"lock-token-submitted",
|
||||
"locktype",
|
||||
"multistatus",
|
||||
"no-conflicting-lock",
|
||||
"no-external-entities",
|
||||
"owner",
|
||||
"preserved-live-properties",
|
||||
"prop",
|
||||
"propertyupdate",
|
||||
"propfind",
|
||||
"propfind-finite-depth",
|
||||
"propname",
|
||||
"propstat",
|
||||
"remove",
|
||||
"resourcetype",
|
||||
"response",
|
||||
"responsedescription",
|
||||
"set",
|
||||
"shared",
|
||||
"status",
|
||||
"supportedlock",
|
||||
"text/html",
|
||||
"timeout",
|
||||
"write",
|
||||
];
|
||||
|
||||
#[derive(Arbitrary)]
|
||||
enum Token {
|
||||
Known(usize),
|
||||
//Unknown(String),
|
||||
}
|
||||
impl Token {
|
||||
fn serialize(&self) -> String {
|
||||
match self {
|
||||
Self::Known(i) => tokens[i % tokens.len()].to_string(),
|
||||
//Self::Unknown(v) => v.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Arbitrary)]
|
||||
struct Tag {
|
||||
//prefix: Option<Token>,
|
||||
name: Token,
|
||||
attr: Option<(Token, Token)>,
|
||||
}
|
||||
impl Tag {
|
||||
fn start(&self) -> String {
|
||||
let mut acc = String::new();
|
||||
/*if let Some(p) = &self.prefix {
|
||||
acc.push_str(p.serialize().as_str());
|
||||
acc.push_str(":");
|
||||
}*/
|
||||
acc.push_str("D:");
|
||||
acc.push_str(self.name.serialize().as_str());
|
||||
|
||||
if let Some((k,v)) = &self.attr {
|
||||
acc.push_str(" ");
|
||||
acc.push_str(k.serialize().as_str());
|
||||
acc.push_str("=\"");
|
||||
acc.push_str(v.serialize().as_str());
|
||||
acc.push_str("\"");
|
||||
}
|
||||
acc
|
||||
}
|
||||
fn end(&self) -> String {
|
||||
let mut acc = String::new();
|
||||
acc.push_str("D:");
|
||||
acc.push_str(self.name.serialize().as_str());
|
||||
acc
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Arbitrary)]
|
||||
enum XmlNode {
|
||||
//@FIXME: build RFC3339 and RFC822 Dates with chrono based on timestamps
|
||||
//@FIXME: add small numbers
|
||||
//@FIXME: add http status code
|
||||
Node(Tag, Vec<Self>),
|
||||
Number(u64),
|
||||
Text(Token),
|
||||
}
|
||||
impl std::fmt::Debug for XmlNode {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.serialize())
|
||||
}
|
||||
}
|
||||
impl XmlNode {
|
||||
fn serialize(&self) -> String {
|
||||
match self {
|
||||
Self::Node(tag, children) => {
|
||||
let stag = tag.start();
|
||||
match children.is_empty() {
|
||||
true => format!("<{}/>", stag),
|
||||
false => format!("<{}>{}</{}>", stag, children.iter().map(|v| v.serialize()).collect::<String>(), tag.end()),
|
||||
}
|
||||
},
|
||||
Self::Number(v) => format!("{}", v),
|
||||
Self::Text(v) => v.serialize(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn serialize(elem: &impl xml::QWrite) -> Vec<u8> {
|
||||
let mut buffer = Vec::new();
|
||||
let mut tokio_buffer = tokio::io::BufWriter::new(&mut buffer);
|
||||
let q = quick_xml::writer::Writer::new_with_indent(&mut tokio_buffer, b' ', 4);
|
||||
let ns_to_apply = vec![ ("xmlns:D".into(), "DAV:".into()) ];
|
||||
let mut writer = xml::Writer { q, ns_to_apply };
|
||||
|
||||
elem.qwrite(&mut writer).await.expect("xml serialization");
|
||||
tokio_buffer.flush().await.expect("tokio buffer flush");
|
||||
|
||||
return buffer
|
||||
}
|
||||
|
||||
type Object = types::Multistatus<realization::Core, types::PropValue<realization::Core>>;
|
||||
|
||||
fuzz_target!(|nodes: XmlNode| {
|
||||
let gen = format!("<D:multistatus xmlns:D=\"DAV:\">{}<D:/multistatus>", nodes.serialize());
|
||||
//println!("--------\n{}", gen);
|
||||
let data = gen.as_bytes();
|
||||
|
||||
let rt = Runtime::new().expect("tokio runtime initialization");
|
||||
|
||||
rt.block_on(async {
|
||||
// 1. Setup fuzzing by finding an input that seems correct, do not crash yet then.
|
||||
let mut rdr = match xml::Reader::new(NsReader::from_reader(data)).await {
|
||||
Err(_) => return,
|
||||
Ok(r) => r,
|
||||
};
|
||||
let reference = match rdr.find::<Object>().await {
|
||||
Err(_) => return,
|
||||
Ok(m) => m,
|
||||
};
|
||||
|
||||
// 2. Re-serialize the input
|
||||
let my_serialization = serialize(&reference).await;
|
||||
|
||||
// 3. De-serialize my serialization
|
||||
let mut rdr2 = xml::Reader::new(NsReader::from_reader(my_serialization.as_slice())).await.expect("XML Reader init");
|
||||
let comparison = rdr2.find::<Object>().await.expect("Deserialize again");
|
||||
|
||||
// 4. Both the first decoding and last decoding must be identical
|
||||
assert_eq!(reference, comparison);
|
||||
})
|
||||
});
|
|
@ -0,0 +1,68 @@
|
|||
use super::acltypes::*;
|
||||
use super::types as dav;
|
||||
use super::xml::{QRead, Reader, IRead, DAV_URN};
|
||||
use super::error::ParsingError;
|
||||
|
||||
impl QRead<Property> for Property {
|
||||
async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
|
||||
if xml.maybe_open_start(DAV_URN, "owner").await?.is_some() {
|
||||
let href = xml.find().await?;
|
||||
xml.close().await?;
|
||||
return Ok(Self::Owner(href))
|
||||
}
|
||||
if xml.maybe_open_start(DAV_URN, "current-user-principal").await?.is_some() {
|
||||
let user = xml.find().await?;
|
||||
xml.close().await?;
|
||||
return Ok(Self::CurrentUserPrincipal(user))
|
||||
}
|
||||
if xml.maybe_open_start(DAV_URN, "current-user-privilege-set").await?.is_some() {
|
||||
xml.close().await?;
|
||||
return Ok(Self::CurrentUserPrivilegeSet(vec![]))
|
||||
}
|
||||
|
||||
Err(ParsingError::Recoverable)
|
||||
}
|
||||
}
|
||||
|
||||
impl QRead<PropertyRequest> for PropertyRequest {
|
||||
async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
|
||||
if xml.maybe_open(DAV_URN, "owner").await?.is_some() {
|
||||
xml.close().await?;
|
||||
return Ok(Self::Owner)
|
||||
}
|
||||
|
||||
if xml.maybe_open(DAV_URN, "current-user-principal").await?.is_some() {
|
||||
xml.close().await?;
|
||||
return Ok(Self::CurrentUserPrincipal)
|
||||
}
|
||||
|
||||
if xml.maybe_open(DAV_URN, "current-user-privilege-set").await?.is_some() {
|
||||
xml.close().await?;
|
||||
return Ok(Self::CurrentUserPrivilegeSet)
|
||||
}
|
||||
|
||||
Err(ParsingError::Recoverable)
|
||||
}
|
||||
}
|
||||
|
||||
impl QRead<ResourceType> for ResourceType {
|
||||
async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
|
||||
if xml.maybe_open(DAV_URN, "principal").await?.is_some() {
|
||||
xml.close().await?;
|
||||
return Ok(Self::Principal)
|
||||
}
|
||||
Err(ParsingError::Recoverable)
|
||||
}
|
||||
}
|
||||
|
||||
// -----
|
||||
impl QRead<User> for User {
|
||||
async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
|
||||
if xml.maybe_open(DAV_URN, "unauthenticated").await?.is_some() {
|
||||
xml.close().await?;
|
||||
return Ok(Self::Unauthenticated)
|
||||
}
|
||||
|
||||
dav::Href::qread(xml).await.map(Self::Authenticated)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
use quick_xml::Error as QError;
|
||||
use quick_xml::events::Event;
|
||||
|
||||
use super::acltypes::*;
|
||||
use super::xml::{QWrite, Writer, IWrite};
|
||||
use super::error::ParsingError;
|
||||
|
||||
impl QWrite for Property {
|
||||
async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
|
||||
match self {
|
||||
Self::Owner(href) => {
|
||||
let start = xml.create_dav_element("owner");
|
||||
let end = start.to_end();
|
||||
xml.q.write_event_async(Event::Start(start.clone())).await?;
|
||||
href.qwrite(xml).await?;
|
||||
xml.q.write_event_async(Event::End(end)).await
|
||||
},
|
||||
Self::CurrentUserPrincipal(user) => {
|
||||
let start = xml.create_dav_element("current-user-principal");
|
||||
let end = start.to_end();
|
||||
xml.q.write_event_async(Event::Start(start.clone())).await?;
|
||||
user.qwrite(xml).await?;
|
||||
xml.q.write_event_async(Event::End(end)).await
|
||||
},
|
||||
Self::CurrentUserPrivilegeSet(_) => {
|
||||
let empty_tag = xml.create_dav_element("current-user-privilege-set");
|
||||
xml.q.write_event_async(Event::Empty(empty_tag)).await
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl QWrite for PropertyRequest {
|
||||
async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
|
||||
let mut atom = async |c| {
|
||||
let empty_tag = xml.create_dav_element(c);
|
||||
xml.q.write_event_async(Event::Empty(empty_tag)).await
|
||||
};
|
||||
|
||||
match self {
|
||||
Self::Owner => atom("owner").await,
|
||||
Self::CurrentUserPrincipal => atom("current-user-principal").await,
|
||||
Self::CurrentUserPrivilegeSet => atom("current-user-privilege-set").await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl QWrite for ResourceType {
|
||||
async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
|
||||
match self {
|
||||
Self::Principal => {
|
||||
let empty_tag = xml.create_dav_element("principal");
|
||||
xml.q.write_event_async(Event::Empty(empty_tag)).await
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -----
|
||||
|
||||
impl QWrite for User {
|
||||
async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
|
||||
match self {
|
||||
Self::Unauthenticated => {
|
||||
let tag = xml.create_dav_element("unauthenticated");
|
||||
xml.q.write_event_async(Event::Empty(tag)).await
|
||||
},
|
||||
Self::Authenticated(href) => href.qwrite(xml).await,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
use super::types as dav;
|
||||
|
||||
//RFC covered: RFC3744 (ACL core) + RFC5397 (ACL Current Principal Extension)
|
||||
|
||||
|
||||
//@FIXME required for a full CalDAV implementation
|
||||
// See section 6. of the CalDAV RFC
|
||||
// It seems mainly required for free-busy that I will not implement now.
|
||||
// It can also be used for discovering main calendar, not sure it is used.
|
||||
// Note: it is used by Thunderbird
|
||||
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum PropertyRequest {
|
||||
Owner,
|
||||
CurrentUserPrincipal,
|
||||
CurrentUserPrivilegeSet,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum Property {
|
||||
Owner(dav::Href),
|
||||
CurrentUserPrincipal(User),
|
||||
CurrentUserPrivilegeSet(Vec<Privilege>),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum ResourceType {
|
||||
Principal,
|
||||
}
|
||||
|
||||
/// Not implemented, it's a placeholder
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct Privilege(());
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum User {
|
||||
Unauthenticated,
|
||||
Authenticated(dav::Href),
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,927 @@
|
|||
use quick_xml::Error as QError;
|
||||
use quick_xml::events::{Event, BytesText};
|
||||
|
||||
use super::caltypes::*;
|
||||
use super::xml::{Node, QWrite, IWrite, Writer};
|
||||
use super::types::Extension;
|
||||
|
||||
|
||||
// ==================== Calendar Types Serialization =========================
|
||||
|
||||
// -------------------- MKCALENDAR METHOD ------------------------------------
|
||||
impl<E: Extension> QWrite for MkCalendar<E> {
|
||||
async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
|
||||
let start = xml.create_cal_element("mkcalendar");
|
||||
let end = start.to_end();
|
||||
|
||||
xml.q.write_event_async(Event::Start(start.clone())).await?;
|
||||
self.0.qwrite(xml).await?;
|
||||
xml.q.write_event_async(Event::End(end)).await
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Extension> QWrite for MkCalendarResponse<E> {
|
||||
async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
|
||||
let start = xml.create_cal_element("mkcalendar-response");
|
||||
let end = start.to_end();
|
||||
|
||||
xml.q.write_event_async(Event::Start(start.clone())).await?;
|
||||
for propstat in self.0.iter() {
|
||||
propstat.qwrite(xml).await?;
|
||||
}
|
||||
xml.q.write_event_async(Event::End(end)).await
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------- REPORT METHOD -------------------------------------
|
||||
impl<E: Extension> QWrite for Report<E> {
|
||||
async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
|
||||
match self {
|
||||
Self::Query(v) => v.qwrite(xml).await,
|
||||
Self::Multiget(v) => v.qwrite(xml).await,
|
||||
Self::FreeBusy(v) => v.qwrite(xml).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Extension> QWrite for CalendarQuery<E> {
|
||||
async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
|
||||
let start = xml.create_cal_element("calendar-query");
|
||||
let end = start.to_end();
|
||||
|
||||
xml.q.write_event_async(Event::Start(start.clone())).await?;
|
||||
if let Some(selector) = &self.selector {
|
||||
selector.qwrite(xml).await?;
|
||||
}
|
||||
self.filter.qwrite(xml).await?;
|
||||
if let Some(tz) = &self.timezone {
|
||||
tz.qwrite(xml).await?;
|
||||
}
|
||||
xml.q.write_event_async(Event::End(end)).await
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Extension> QWrite for CalendarMultiget<E> {
|
||||
async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
|
||||
let start = xml.create_cal_element("calendar-multiget");
|
||||
let end = start.to_end();
|
||||
|
||||
xml.q.write_event_async(Event::Start(start.clone())).await?;
|
||||
if let Some(selector) = &self.selector {
|
||||
selector.qwrite(xml).await?;
|
||||
}
|
||||
for href in self.href.iter() {
|
||||
href.qwrite(xml).await?;
|
||||
}
|
||||
xml.q.write_event_async(Event::End(end)).await
|
||||
}
|
||||
}
|
||||
|
||||
impl QWrite for FreeBusyQuery {
|
||||
async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
|
||||
let start = xml.create_cal_element("free-busy-query");
|
||||
let end = start.to_end();
|
||||
|
||||
xml.q.write_event_async(Event::Start(start.clone())).await?;
|
||||
self.0.qwrite(xml).await?;
|
||||
xml.q.write_event_async(Event::End(end)).await
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------- DAV::prop --------------------------------------
|
||||
impl QWrite for PropertyRequest {
|
||||
async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
|
||||
let mut atom = async |c| {
|
||||
let empty_tag = xml.create_cal_element(c);
|
||||
xml.q.write_event_async(Event::Empty(empty_tag)).await
|
||||
};
|
||||
|
||||
match self {
|
||||
Self::CalendarHomeSet => atom("calendar-home-set").await,
|
||||
Self::CalendarDescription => atom("calendar-description").await,
|
||||
Self::CalendarTimezone => atom("calendar-timezone").await,
|
||||
Self::SupportedCalendarComponentSet => atom("supported-calendar-component-set").await,
|
||||
Self::SupportedCalendarData => atom("supported-calendar-data").await,
|
||||
Self::MaxResourceSize => atom("max-resource-size").await,
|
||||
Self::MinDateTime => atom("min-date-time").await,
|
||||
Self::MaxDateTime => atom("max-date-time").await,
|
||||
Self::MaxInstances => atom("max-instances").await,
|
||||
Self::MaxAttendeesPerInstance => atom("max-attendees-per-instance").await,
|
||||
Self::SupportedCollationSet => atom("supported-collation-set").await,
|
||||
Self::CalendarData(req) => req.qwrite(xml).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl QWrite for Property {
|
||||
async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
|
||||
match self {
|
||||
Self::CalendarHomeSet(href) => {
|
||||
let start = xml.create_cal_element("calendar-home-set");
|
||||
let end = start.to_end();
|
||||
xml.q.write_event_async(Event::Start(start.clone())).await?;
|
||||
href.qwrite(xml).await?;
|
||||
xml.q.write_event_async(Event::End(end)).await
|
||||
}
|
||||
Self::CalendarDescription { lang, text } => {
|
||||
let mut start = xml.create_cal_element("calendar-description");
|
||||
if let Some(the_lang) = lang {
|
||||
start.push_attribute(("xml:lang", the_lang.as_str()));
|
||||
}
|
||||
let end = start.to_end();
|
||||
|
||||
xml.q.write_event_async(Event::Start(start.clone())).await?;
|
||||
xml.q.write_event_async(Event::Text(BytesText::new(text))).await?;
|
||||
xml.q.write_event_async(Event::End(end)).await
|
||||
},
|
||||
Self::CalendarTimezone(payload) => {
|
||||
let start = xml.create_cal_element("calendar-timezone");
|
||||
let end = start.to_end();
|
||||
|
||||
xml.q.write_event_async(Event::Start(start.clone())).await?;
|
||||
xml.q.write_event_async(Event::Text(BytesText::new(payload))).await?;
|
||||
xml.q.write_event_async(Event::End(end)).await
|
||||
},
|
||||
Self::SupportedCalendarComponentSet(many_comp) => {
|
||||
let start = xml.create_cal_element("supported-calendar-component-set");
|
||||
let end = start.to_end();
|
||||
|
||||
xml.q.write_event_async(Event::Start(start.clone())).await?;
|
||||
for comp in many_comp.iter() {
|
||||
comp.qwrite(xml).await?;
|
||||
}
|
||||
xml.q.write_event_async(Event::End(end)).await
|
||||
},
|
||||
Self::SupportedCalendarData(many_mime) => {
|
||||
let start = xml.create_cal_element("supported-calendar-data");
|
||||
let end = start.to_end();
|
||||
|
||||
xml.q.write_event_async(Event::Start(start.clone())).await?;
|
||||
for mime in many_mime.iter() {
|
||||
mime.qwrite(xml).await?;
|
||||
}
|
||||
xml.q.write_event_async(Event::End(end)).await
|
||||
},
|
||||
Self::MaxResourceSize(bytes) => {
|
||||
let start = xml.create_cal_element("max-resource-size");
|
||||
let end = start.to_end();
|
||||
|
||||
xml.q.write_event_async(Event::Start(start.clone())).await?;
|
||||
xml.q.write_event_async(Event::Text(BytesText::new(bytes.to_string().as_str()))).await?;
|
||||
xml.q.write_event_async(Event::End(end)).await
|
||||
},
|
||||
Self::MinDateTime(dt) => {
|
||||
let start = xml.create_cal_element("min-date-time");
|
||||
let end = start.to_end();
|
||||
|
||||
let dtstr = format!("{}", dt.format(ICAL_DATETIME_FMT));
|
||||
xml.q.write_event_async(Event::Start(start.clone())).await?;
|
||||
xml.q.write_event_async(Event::Text(BytesText::new(dtstr.as_str()))).await?;
|
||||
xml.q.write_event_async(Event::End(end)).await
|
||||
},
|
||||
Self::MaxDateTime(dt) => {
|
||||
let start = xml.create_cal_element("max-date-time");
|
||||
let end = start.to_end();
|
||||
|
||||
let dtstr = format!("{}", dt.format(ICAL_DATETIME_FMT));
|
||||
xml.q.write_event_async(Event::Start(start.clone())).await?;
|
||||
xml.q.write_event_async(Event::Text(BytesText::new(dtstr.as_str()))).await?;
|
||||
xml.q.write_event_async(Event::End(end)).await
|
||||
},
|
||||
Self::MaxInstances(count) => {
|
||||
let start = xml.create_cal_element("max-instances");
|
||||
let end = start.to_end();
|
||||
|
||||
xml.q.write_event_async(Event::Start(start.clone())).await?;
|
||||
xml.q.write_event_async(Event::Text(BytesText::new(count.to_string().as_str()))).await?;
|
||||
xml.q.write_event_async(Event::End(end)).await
|
||||
},
|
||||
Self::MaxAttendeesPerInstance(count) => {
|
||||
let start = xml.create_cal_element("max-attendees-per-instance");
|
||||
let end = start.to_end();
|
||||
|
||||
xml.q.write_event_async(Event::Start(start.clone())).await?;
|
||||
xml.q.write_event_async(Event::Text(BytesText::new(count.to_string().as_str()))).await?;
|
||||
xml.q.write_event_async(Event::End(end)).await
|
||||
},
|
||||
Self::SupportedCollationSet(many_collations) => {
|
||||
let start = xml.create_cal_element("supported-collation-set");
|
||||
let end = start.to_end();
|
||||
|
||||
xml.q.write_event_async(Event::Start(start.clone())).await?;
|
||||
for collation in many_collations.iter() {
|
||||
collation.qwrite(xml).await?;
|
||||
}
|
||||
xml.q.write_event_async(Event::End(end)).await
|
||||
},
|
||||
Self::CalendarData(inner) => inner.qwrite(xml).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------- DAV::resourcetype ----------------------------------
|
||||
impl QWrite for ResourceType {
|
||||
async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
|
||||
match self {
|
||||
Self::Calendar => {
|
||||
let empty_tag = xml.create_cal_element("calendar");
|
||||
xml.q.write_event_async(Event::Empty(empty_tag)).await
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------- DAV::error ------------------------------------
|
||||
impl QWrite for Violation {
|
||||
async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
|
||||
let mut atom = async |c| {
|
||||
let empty_tag = xml.create_cal_element(c);
|
||||
xml.q.write_event_async(Event::Empty(empty_tag)).await
|
||||
};
|
||||
|
||||
match self {
|
||||
//@FIXME
|
||||
// DAV elements, should not be here but in RFC3744 on ACLs
|
||||
// (we do not use atom as this error is in the DAV namespace, not the caldav one)
|
||||
Self::NeedPrivileges => {
|
||||
let empty_tag = xml.create_dav_element("need-privileges");
|
||||
xml.q.write_event_async(Event::Empty(empty_tag)).await
|
||||
},
|
||||
|
||||
// Regular CalDAV errors
|
||||
Self::ResourceMustBeNull => atom("resource-must-be-null").await,
|
||||
Self::CalendarCollectionLocationOk => atom("calendar-collection-location-ok").await,
|
||||
Self::ValidCalendarData => atom("valid-calendar-data").await,
|
||||
Self::InitializeCalendarCollection => atom("initialize-calendar-collection").await,
|
||||
Self::SupportedCalendarData => atom("supported-calendar-data").await,
|
||||
Self::ValidCalendarObjectResource => atom("valid-calendar-object-resource").await,
|
||||
Self::SupportedCalendarComponent => atom("supported-calendar-component").await,
|
||||
Self::NoUidConflict(href) => {
|
||||
let start = xml.create_cal_element("no-uid-conflict");
|
||||
let end = start.to_end();
|
||||
|
||||
xml.q.write_event_async(Event::Start(start.clone())).await?;
|
||||
href.qwrite(xml).await?;
|
||||
xml.q.write_event_async(Event::End(end)).await
|
||||
},
|
||||
Self::MaxResourceSize => atom("max-resource-size").await,
|
||||
Self::MinDateTime => atom("min-date-time").await,
|
||||
Self::MaxDateTime => atom("max-date-time").await,
|
||||
Self::MaxInstances => atom("max-instances").await,
|
||||
Self::MaxAttendeesPerInstance => atom("max-attendees-per-instance").await,
|
||||
Self::ValidFilter => atom("valid-filter").await,
|
||||
Self::SupportedFilter { comp, prop, param } => {
|
||||
let start = xml.create_cal_element("supported-filter");
|
||||
let end = start.to_end();
|
||||
|
||||
xml.q.write_event_async(Event::Start(start.clone())).await?;
|
||||
for comp_item in comp.iter() {
|
||||
comp_item.qwrite(xml).await?;
|
||||
}
|
||||
for prop_item in prop.iter() {
|
||||
prop_item.qwrite(xml).await?;
|
||||
}
|
||||
for param_item in param.iter() {
|
||||
param_item.qwrite(xml).await?;
|
||||
}
|
||||
xml.q.write_event_async(Event::End(end)).await
|
||||
},
|
||||
Self::NumberOfMatchesWithinLimits => atom("number-of-matches-within-limits").await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ---------------------------- Inner XML ------------------------------------
|
||||
impl QWrite for SupportedCollation {
|
||||
async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
|
||||
let start = xml.create_cal_element("supported-collation");
|
||||
let end = start.to_end();
|
||||
|
||||
xml.q.write_event_async(Event::Start(start.clone())).await?;
|
||||
self.0.qwrite(xml).await?;
|
||||
xml.q.write_event_async(Event::End(end)).await
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
impl QWrite for Collation {
|
||||
async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
|
||||
let col = match self {
|
||||
Self::AsciiCaseMap => "i;ascii-casemap",
|
||||
Self::Octet => "i;octet",
|
||||
Self::Unknown(v) => v.as_str(),
|
||||
};
|
||||
|
||||
xml.q.write_event_async(Event::Text(BytesText::new(col))).await
|
||||
}
|
||||
}
|
||||
|
||||
impl QWrite for CalendarDataSupport {
|
||||
async fn qwrite(&self, _xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
|
||||
unreachable!();
|
||||
}
|
||||
}
|
||||
|
||||
impl QWrite for CalendarDataPayload {
|
||||
async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
|
||||
let mut start = xml.create_cal_element("calendar-data");
|
||||
if let Some(mime) = &self.mime {
|
||||
start.push_attribute(("content-type", mime.content_type.as_str()));
|
||||
start.push_attribute(("version", mime.version.as_str()));
|
||||
}
|
||||
let end = start.to_end();
|
||||
|
||||
xml.q.write_event_async(Event::Start(start.clone())).await?;
|
||||
xml.q.write_event_async(Event::Text(BytesText::new(self.payload.as_str()))).await?;
|
||||
xml.q.write_event_async(Event::End(end)).await
|
||||
}
|
||||
}
|
||||
|
||||
impl QWrite for CalendarDataRequest {
|
||||
async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
|
||||
let mut start = xml.create_cal_element("calendar-data");
|
||||
if let Some(mime) = &self.mime {
|
||||
start.push_attribute(("content-type", mime.content_type.as_str()));
|
||||
start.push_attribute(("version", mime.version.as_str()));
|
||||
}
|
||||
|
||||
// Empty tag
|
||||
if self.comp.is_none() && self.recurrence.is_none() && self.limit_freebusy_set.is_none() {
|
||||
return xml.q.write_event_async(Event::Empty(start.clone())).await
|
||||
}
|
||||
|
||||
let end = start.to_end();
|
||||
xml.q.write_event_async(Event::Start(start.clone())).await?;
|
||||
if let Some(comp) = &self.comp {
|
||||
comp.qwrite(xml).await?;
|
||||
}
|
||||
if let Some(recurrence) = &self.recurrence {
|
||||
recurrence.qwrite(xml).await?;
|
||||
}
|
||||
if let Some(freebusy) = &self.limit_freebusy_set {
|
||||
freebusy.qwrite(xml).await?;
|
||||
}
|
||||
xml.q.write_event_async(Event::End(end)).await
|
||||
}
|
||||
}
|
||||
|
||||
impl QWrite for CalendarDataEmpty {
|
||||
async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
|
||||
let mut empty = xml.create_cal_element("calendar-data");
|
||||
if let Some(mime) = &self.0 {
|
||||
empty.push_attribute(("content-type", mime.content_type.as_str()));
|
||||
empty.push_attribute(("version", mime.version.as_str()));
|
||||
}
|
||||
xml.q.write_event_async(Event::Empty(empty)).await
|
||||
}
|
||||
}
|
||||
|
||||
impl QWrite for Comp {
|
||||
async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
|
||||
let mut start = xml.create_cal_element("comp");
|
||||
start.push_attribute(("name", self.name.as_str()));
|
||||
match (&self.prop_kind, &self.comp_kind) {
|
||||
(None, None) => xml.q.write_event_async(Event::Empty(start)).await,
|
||||
_ => {
|
||||
let end = start.to_end();
|
||||
xml.q.write_event_async(Event::Start(start.clone())).await?;
|
||||
if let Some(prop_kind) = &self.prop_kind {
|
||||
prop_kind.qwrite(xml).await?;
|
||||
}
|
||||
if let Some(comp_kind) = &self.comp_kind {
|
||||
comp_kind.qwrite(xml).await?;
|
||||
}
|
||||
xml.q.write_event_async(Event::End(end)).await
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl QWrite for CompSupport {
|
||||
async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
|
||||
let mut empty = xml.create_cal_element("comp");
|
||||
empty.push_attribute(("name", self.0.as_str()));
|
||||
xml.q.write_event_async(Event::Empty(empty)).await
|
||||
}
|
||||
}
|
||||
|
||||
impl QWrite for CompKind {
|
||||
async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
|
||||
match self {
|
||||
Self::AllComp => {
|
||||
let empty_tag = xml.create_cal_element("allcomp");
|
||||
xml.q.write_event_async(Event::Empty(empty_tag)).await
|
||||
},
|
||||
Self::Comp(many_comp) => {
|
||||
for comp in many_comp.iter() {
|
||||
// Required: recursion in an async fn requires boxing
|
||||
// rustc --explain E0733
|
||||
// Cycle detected when computing type of ...
|
||||
// For more information about this error, try `rustc --explain E0391`.
|
||||
// https://github.com/rust-lang/rust/issues/78649
|
||||
#[inline(always)]
|
||||
fn recurse<'a>(comp: &'a Comp, xml: &'a mut Writer<impl IWrite>) -> futures::future::BoxFuture<'a, Result<(), QError>> {
|
||||
Box::pin(comp.qwrite(xml))
|
||||
}
|
||||
recurse(comp, xml).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl QWrite for PropKind {
|
||||
async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
|
||||
match self {
|
||||
Self::AllProp => {
|
||||
let empty_tag = xml.create_cal_element("allprop");
|
||||
xml.q.write_event_async(Event::Empty(empty_tag)).await
|
||||
},
|
||||
Self::Prop(many_prop) => {
|
||||
for prop in many_prop.iter() {
|
||||
prop.qwrite(xml).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl QWrite for CalProp {
|
||||
async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
|
||||
let mut empty = xml.create_cal_element("prop");
|
||||
empty.push_attribute(("name", self.name.0.as_str()));
|
||||
match self.novalue {
|
||||
None => (),
|
||||
Some(true) => empty.push_attribute(("novalue", "yes")),
|
||||
Some(false) => empty.push_attribute(("novalue", "no")),
|
||||
}
|
||||
xml.q.write_event_async(Event::Empty(empty)).await
|
||||
}
|
||||
}
|
||||
|
||||
impl QWrite for RecurrenceModifier {
|
||||
async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
|
||||
match self {
|
||||
Self::Expand(exp) => exp.qwrite(xml).await,
|
||||
Self::LimitRecurrenceSet(lrs) => lrs.qwrite(xml).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl QWrite for Expand {
|
||||
async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
|
||||
let mut empty = xml.create_cal_element("expand");
|
||||
empty.push_attribute(("start", format!("{}", self.0.format(ICAL_DATETIME_FMT)).as_str()));
|
||||
empty.push_attribute(("end", format!("{}", self.1.format(ICAL_DATETIME_FMT)).as_str()));
|
||||
xml.q.write_event_async(Event::Empty(empty)).await
|
||||
}
|
||||
}
|
||||
|
||||
impl QWrite for LimitRecurrenceSet {
|
||||
async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
|
||||
let mut empty = xml.create_cal_element("limit-recurrence-set");
|
||||
empty.push_attribute(("start", format!("{}", self.0.format(ICAL_DATETIME_FMT)).as_str()));
|
||||
empty.push_attribute(("end", format!("{}", self.1.format(ICAL_DATETIME_FMT)).as_str()));
|
||||
xml.q.write_event_async(Event::Empty(empty)).await
|
||||
}
|
||||
}
|
||||
|
||||
impl QWrite for LimitFreebusySet {
|
||||
async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
|
||||
let mut empty = xml.create_cal_element("limit-freebusy-set");
|
||||
empty.push_attribute(("start", format!("{}", self.0.format(ICAL_DATETIME_FMT)).as_str()));
|
||||
empty.push_attribute(("end", format!("{}", self.1.format(ICAL_DATETIME_FMT)).as_str()));
|
||||
xml.q.write_event_async(Event::Empty(empty)).await
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Extension> QWrite for CalendarSelector<E> {
|
||||
async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
|
||||
match self {
|
||||
Self::AllProp => {
|
||||
let empty_tag = xml.create_dav_element("allprop");
|
||||
xml.q.write_event_async(Event::Empty(empty_tag)).await
|
||||
},
|
||||
Self::PropName => {
|
||||
let empty_tag = xml.create_dav_element("propname");
|
||||
xml.q.write_event_async(Event::Empty(empty_tag)).await
|
||||
},
|
||||
Self::Prop(prop) => prop.qwrite(xml).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl QWrite for CompFilter {
|
||||
async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
|
||||
let mut start = xml.create_cal_element("comp-filter");
|
||||
start.push_attribute(("name", self.name.as_str()));
|
||||
|
||||
match &self.additional_rules {
|
||||
None => xml.q.write_event_async(Event::Empty(start)).await,
|
||||
Some(rules) => {
|
||||
let end = start.to_end();
|
||||
|
||||
xml.q.write_event_async(Event::Start(start.clone())).await?;
|
||||
rules.qwrite(xml).await?;
|
||||
xml.q.write_event_async(Event::End(end)).await
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl QWrite for CompFilterRules {
|
||||
async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
|
||||
match self {
|
||||
Self::IsNotDefined => {
|
||||
let empty_tag = xml.create_dav_element("is-not-defined");
|
||||
xml.q.write_event_async(Event::Empty(empty_tag)).await
|
||||
},
|
||||
Self::Matches(cfm) => cfm.qwrite(xml).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl QWrite for CompFilterMatch {
|
||||
async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
|
||||
if let Some(time_range) = &self.time_range {
|
||||
time_range.qwrite(xml).await?;
|
||||
}
|
||||
|
||||
for prop_item in self.prop_filter.iter() {
|
||||
prop_item.qwrite(xml).await?;
|
||||
}
|
||||
for comp_item in self.comp_filter.iter() {
|
||||
// Required: recursion in an async fn requires boxing
|
||||
// rustc --explain E0733
|
||||
// Cycle detected when computing type of ...
|
||||
// For more information about this error, try `rustc --explain E0391`.
|
||||
// https://github.com/rust-lang/rust/issues/78649
|
||||
#[inline(always)]
|
||||
fn recurse<'a>(comp: &'a CompFilter, xml: &'a mut Writer<impl IWrite>) -> futures::future::BoxFuture<'a, Result<(), QError>> {
|
||||
Box::pin(comp.qwrite(xml))
|
||||
}
|
||||
recurse(comp_item, xml).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl QWrite for PropFilter {
|
||||
async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
|
||||
let mut start = xml.create_cal_element("prop-filter");
|
||||
start.push_attribute(("name", self.name.0.as_str()));
|
||||
|
||||
match &self.additional_rules {
|
||||
None => xml.q.write_event_async(Event::Empty(start.clone())).await,
|
||||
Some(rules) => {
|
||||
let end = start.to_end();
|
||||
xml.q.write_event_async(Event::Start(start.clone())).await?;
|
||||
rules.qwrite(xml).await?;
|
||||
xml.q.write_event_async(Event::End(end)).await
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl QWrite for PropFilterRules {
|
||||
async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
|
||||
match self {
|
||||
Self::IsNotDefined => {
|
||||
let empty_tag = xml.create_dav_element("is-not-defined");
|
||||
xml.q.write_event_async(Event::Empty(empty_tag)).await
|
||||
},
|
||||
Self::Match(prop_match) => prop_match.qwrite(xml).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl QWrite for PropFilterMatch {
|
||||
async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
|
||||
if let Some(time_range) = &self.time_range {
|
||||
time_range.qwrite(xml).await?;
|
||||
}
|
||||
if let Some(time_or_text) = &self.time_or_text {
|
||||
time_or_text.qwrite(xml).await?;
|
||||
}
|
||||
for param_item in self.param_filter.iter() {
|
||||
param_item.qwrite(xml).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl QWrite for TimeOrText {
|
||||
async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
|
||||
match self {
|
||||
Self::Time(time) => time.qwrite(xml).await,
|
||||
Self::Text(txt) => txt.qwrite(xml).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl QWrite for TextMatch {
|
||||
async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
|
||||
let mut start = xml.create_cal_element("text-match");
|
||||
if let Some(collation) = &self.collation {
|
||||
start.push_attribute(("collation", collation.as_str()));
|
||||
}
|
||||
match self.negate_condition {
|
||||
None => (),
|
||||
Some(true) => start.push_attribute(("negate-condition", "yes")),
|
||||
Some(false) => start.push_attribute(("negate-condition", "no")),
|
||||
}
|
||||
let end = start.to_end();
|
||||
|
||||
xml.q.write_event_async(Event::Start(start.clone())).await?;
|
||||
xml.q.write_event_async(Event::Text(BytesText::new(self.text.as_str()))).await?;
|
||||
xml.q.write_event_async(Event::End(end)).await
|
||||
}
|
||||
}
|
||||
|
||||
impl QWrite for ParamFilter {
|
||||
async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
|
||||
let mut start = xml.create_cal_element("param-filter");
|
||||
start.push_attribute(("name", self.name.as_str()));
|
||||
|
||||
match &self.additional_rules {
|
||||
None => xml.q.write_event_async(Event::Empty(start)).await,
|
||||
Some(rules) => {
|
||||
let end = start.to_end();
|
||||
xml.q.write_event_async(Event::Start(start.clone())).await?;
|
||||
rules.qwrite(xml).await?;
|
||||
xml.q.write_event_async(Event::End(end)).await
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl QWrite for ParamFilterMatch {
|
||||
async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
|
||||
match self {
|
||||
Self::IsNotDefined => {
|
||||
let empty_tag = xml.create_dav_element("is-not-defined");
|
||||
xml.q.write_event_async(Event::Empty(empty_tag)).await
|
||||
},
|
||||
Self::Match(tm) => tm.qwrite(xml).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl QWrite for TimeZone {
|
||||
async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
|
||||
let start = xml.create_cal_element("timezone");
|
||||
let end = start.to_end();
|
||||
|
||||
xml.q.write_event_async(Event::Start(start.clone())).await?;
|
||||
xml.q.write_event_async(Event::Text(BytesText::new(self.0.as_str()))).await?;
|
||||
xml.q.write_event_async(Event::End(end)).await
|
||||
}
|
||||
}
|
||||
|
||||
impl QWrite for Filter {
|
||||
async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
|
||||
let start = xml.create_cal_element("filter");
|
||||
let end = start.to_end();
|
||||
|
||||
xml.q.write_event_async(Event::Start(start.clone())).await?;
|
||||
self.0.qwrite(xml).await?;
|
||||
xml.q.write_event_async(Event::End(end)).await
|
||||
}
|
||||
}
|
||||
|
||||
impl QWrite for TimeRange {
|
||||
async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
|
||||
let mut empty = xml.create_cal_element("time-range");
|
||||
match self {
|
||||
Self::OnlyStart(start) => empty.push_attribute(("start", format!("{}", start.format(ICAL_DATETIME_FMT)).as_str())),
|
||||
Self::OnlyEnd(end) => empty.push_attribute(("end", format!("{}", end.format(ICAL_DATETIME_FMT)).as_str())),
|
||||
Self::FullRange(start, end) => {
|
||||
empty.push_attribute(("start", format!("{}", start.format(ICAL_DATETIME_FMT)).as_str()));
|
||||
empty.push_attribute(("end", format!("{}", end.format(ICAL_DATETIME_FMT)).as_str()));
|
||||
}
|
||||
}
|
||||
xml.q.write_event_async(Event::Empty(empty)).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::types as dav;
|
||||
use crate::realization::Calendar;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use chrono::{Utc,TimeZone};
|
||||
|
||||
async fn serialize(elem: &impl QWrite) -> String {
|
||||
let mut buffer = Vec::new();
|
||||
let mut tokio_buffer = tokio::io::BufWriter::new(&mut buffer);
|
||||
let q = quick_xml::writer::Writer::new_with_indent(&mut tokio_buffer, b' ', 4);
|
||||
let ns_to_apply = vec![
|
||||
("xmlns:D".into(), "DAV:".into()),
|
||||
("xmlns:C".into(), "urn:ietf:params:xml:ns:caldav".into()),
|
||||
];
|
||||
let mut writer = Writer { q, ns_to_apply };
|
||||
|
||||
elem.qwrite(&mut writer).await.expect("xml serialization");
|
||||
tokio_buffer.flush().await.expect("tokio buffer flush");
|
||||
let got = std::str::from_utf8(buffer.as_slice()).unwrap();
|
||||
|
||||
return got.into()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn basic_violation() {
|
||||
let got = serialize(
|
||||
&dav::Error::<Calendar>(vec![
|
||||
dav::Violation::Extension(Violation::ResourceMustBeNull),
|
||||
])
|
||||
).await;
|
||||
|
||||
let expected = r#"<D:error xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
|
||||
<C:resource-must-be-null/>
|
||||
</D:error>"#;
|
||||
|
||||
assert_eq!(&got, expected, "\n---GOT---\n{got}\n---EXP---\n{expected}\n");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn rfc_calendar_query1_req() {
|
||||
let got = serialize(
|
||||
&CalendarQuery::<Calendar> {
|
||||
selector: Some(CalendarSelector::Prop(dav::PropName(vec![
|
||||
dav::PropertyRequest::GetEtag,
|
||||
dav::PropertyRequest::Extension(PropertyRequest::CalendarData(CalendarDataRequest {
|
||||
mime: None,
|
||||
comp: Some(Comp {
|
||||
name: Component::VCalendar,
|
||||
prop_kind: Some(PropKind::Prop(vec![
|
||||
CalProp {
|
||||
name: ComponentProperty("VERSION".into()),
|
||||
novalue: None,
|
||||
}
|
||||
])),
|
||||
comp_kind: Some(CompKind::Comp(vec![
|
||||
Comp {
|
||||
name: Component::VEvent,
|
||||
prop_kind: Some(PropKind::Prop(vec![
|
||||
CalProp { name: ComponentProperty("SUMMARY".into()), novalue: None },
|
||||
CalProp { name: ComponentProperty("UID".into()), novalue: None },
|
||||
CalProp { name: ComponentProperty("DTSTART".into()), novalue: None },
|
||||
CalProp { name: ComponentProperty("DTEND".into()), novalue: None },
|
||||
CalProp { name: ComponentProperty("DURATION".into()), novalue: None },
|
||||
CalProp { name: ComponentProperty("RRULE".into()), novalue: None },
|
||||
CalProp { name: ComponentProperty("RDATE".into()), novalue: None },
|
||||
CalProp { name: ComponentProperty("EXRULE".into()), novalue: None },
|
||||
CalProp { name: ComponentProperty("EXDATE".into()), novalue: None },
|
||||
CalProp { name: ComponentProperty("RECURRENCE-ID".into()), novalue: None },
|
||||
])),
|
||||
comp_kind: None,
|
||||
},
|
||||
Comp {
|
||||
name: Component::VTimeZone,
|
||||
prop_kind: None,
|
||||
comp_kind: None,
|
||||
}
|
||||
])),
|
||||
}),
|
||||
recurrence: None,
|
||||
limit_freebusy_set: None,
|
||||
})),
|
||||
]))),
|
||||
filter: Filter(CompFilter {
|
||||
name: Component::VCalendar,
|
||||
additional_rules: Some(CompFilterRules::Matches(CompFilterMatch {
|
||||
time_range: None,
|
||||
prop_filter: vec![],
|
||||
comp_filter: vec![
|
||||
CompFilter {
|
||||
name: Component::VEvent,
|
||||
additional_rules: Some(CompFilterRules::Matches(CompFilterMatch {
|
||||
time_range: Some(TimeRange::FullRange(
|
||||
Utc.with_ymd_and_hms(2006,1,4,0,0,0).unwrap(),
|
||||
Utc.with_ymd_and_hms(2006,1,5,0,0,0).unwrap(),
|
||||
)),
|
||||
prop_filter: vec![],
|
||||
comp_filter: vec![],
|
||||
})),
|
||||
},
|
||||
],
|
||||
})),
|
||||
}),
|
||||
timezone: None,
|
||||
}
|
||||
).await;
|
||||
|
||||
let expected = r#"<C:calendar-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
|
||||
<D:prop>
|
||||
<D:getetag/>
|
||||
<C:calendar-data>
|
||||
<C:comp name="VCALENDAR">
|
||||
<C:prop name="VERSION"/>
|
||||
<C:comp name="VEVENT">
|
||||
<C:prop name="SUMMARY"/>
|
||||
<C:prop name="UID"/>
|
||||
<C:prop name="DTSTART"/>
|
||||
<C:prop name="DTEND"/>
|
||||
<C:prop name="DURATION"/>
|
||||
<C:prop name="RRULE"/>
|
||||
<C:prop name="RDATE"/>
|
||||
<C:prop name="EXRULE"/>
|
||||
<C:prop name="EXDATE"/>
|
||||
<C:prop name="RECURRENCE-ID"/>
|
||||
</C:comp>
|
||||
<C:comp name="VTIMEZONE"/>
|
||||
</C:comp>
|
||||
</C:calendar-data>
|
||||
</D:prop>
|
||||
<C:filter>
|
||||
<C:comp-filter name="VCALENDAR">
|
||||
<C:comp-filter name="VEVENT">
|
||||
<C:time-range start="20060104T000000Z" end="20060105T000000Z"/>
|
||||
</C:comp-filter>
|
||||
</C:comp-filter>
|
||||
</C:filter>
|
||||
</C:calendar-query>"#;
|
||||
|
||||
assert_eq!(&got, expected, "\n---GOT---\n{got}\n---EXP---\n{expected}\n");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn rfc_calendar_query1_res() {
|
||||
let got = serialize(
|
||||
&dav::Multistatus::<Calendar> {
|
||||
responses: vec![
|
||||
dav::Response {
|
||||
status_or_propstat: dav::StatusOrPropstat::PropStat(
|
||||
dav::Href("http://cal.example.com/bernard/work/abcd2.ics".into()),
|
||||
vec![dav::PropStat {
|
||||
prop: dav::AnyProp(vec![
|
||||
dav::AnyProperty::Value(dav::Property::GetEtag("\"fffff-abcd2\"".into())),
|
||||
dav::AnyProperty::Value(dav::Property::Extension(Property::CalendarData(CalendarDataPayload {
|
||||
mime: None,
|
||||
payload: "PLACEHOLDER".into()
|
||||
}))),
|
||||
]),
|
||||
status: dav::Status(http::status::StatusCode::OK),
|
||||
error: None,
|
||||
responsedescription: None,
|
||||
}]
|
||||
),
|
||||
location: None,
|
||||
error: None,
|
||||
responsedescription: None,
|
||||
},
|
||||
dav::Response {
|
||||
status_or_propstat: dav::StatusOrPropstat::PropStat(
|
||||
dav::Href("http://cal.example.com/bernard/work/abcd3.ics".into()),
|
||||
vec![dav::PropStat {
|
||||
prop: dav::AnyProp(vec![
|
||||
dav::AnyProperty::Value(dav::Property::GetEtag("\"fffff-abcd3\"".into())),
|
||||
dav::AnyProperty::Value(dav::Property::Extension(Property::CalendarData(CalendarDataPayload{
|
||||
mime: None,
|
||||
payload: "PLACEHOLDER".into(),
|
||||
}))),
|
||||
]),
|
||||
status: dav::Status(http::status::StatusCode::OK),
|
||||
error: None,
|
||||
responsedescription: None,
|
||||
}]
|
||||
),
|
||||
location: None,
|
||||
error: None,
|
||||
responsedescription: None,
|
||||
},
|
||||
],
|
||||
responsedescription: None,
|
||||
},
|
||||
).await;
|
||||
|
||||
let expected = r#"<D:multistatus xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
|
||||
<D:response>
|
||||
<D:href>http://cal.example.com/bernard/work/abcd2.ics</D:href>
|
||||
<D:propstat>
|
||||
<D:prop>
|
||||
<D:getetag>"fffff-abcd2"</D:getetag>
|
||||
<C:calendar-data>PLACEHOLDER</C:calendar-data>
|
||||
</D:prop>
|
||||
<D:status>HTTP/1.1 200 OK</D:status>
|
||||
</D:propstat>
|
||||
</D:response>
|
||||
<D:response>
|
||||
<D:href>http://cal.example.com/bernard/work/abcd3.ics</D:href>
|
||||
<D:propstat>
|
||||
<D:prop>
|
||||
<D:getetag>"fffff-abcd3"</D:getetag>
|
||||
<C:calendar-data>PLACEHOLDER</C:calendar-data>
|
||||
</D:prop>
|
||||
<D:status>HTTP/1.1 200 OK</D:status>
|
||||
</D:propstat>
|
||||
</D:response>
|
||||
</D:multistatus>"#;
|
||||
|
||||
|
||||
assert_eq!(&got, expected, "\n---GOT---\n{got}\n---EXP---\n{expected}\n");
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,965 @@
|
|||
use quick_xml::events::Event;
|
||||
use chrono::DateTime;
|
||||
|
||||
use super::types::*;
|
||||
use super::error::ParsingError;
|
||||
use super::xml::{Node, QRead, Reader, IRead, DAV_URN};
|
||||
|
||||
//@TODO (1) Rewrite all objects as Href,
|
||||
// where we return Ok(None) instead of trying to find the object at any cost.
|
||||
// Add a xml.find<E: Qread>() -> Result<Option<E>, ParsingError> or similar for the cases we
|
||||
// really need the object
|
||||
// (2) Rewrite QRead and replace Result<Option<_>, _> with Result<_, _>, not found being a possible
|
||||
// error.
|
||||
// (3) Rewrite vectors with xml.collect<E: QRead>() -> Result<Vec<E>, _>
|
||||
// (4) Something for alternatives like xml::choices on some lib would be great but no idea yet
|
||||
|
||||
// ---- ROOT ----
|
||||
|
||||
/// Propfind request
|
||||
impl<E: Extension> QRead<PropFind<E>> for PropFind<E> {
|
||||
async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
|
||||
xml.open(DAV_URN, "propfind").await?;
|
||||
let propfind: PropFind<E> = loop {
|
||||
// allprop
|
||||
if let Some(_) = xml.maybe_open(DAV_URN, "allprop").await? {
|
||||
xml.close().await?;
|
||||
let includ = xml.maybe_find::<Include<E>>().await?;
|
||||
break PropFind::AllProp(includ)
|
||||
}
|
||||
|
||||
// propname
|
||||
if let Some(_) = xml.maybe_open(DAV_URN, "propname").await? {
|
||||
xml.close().await?;
|
||||
break PropFind::PropName
|
||||
}
|
||||
|
||||
// prop
|
||||
let (mut maybe_prop, mut dirty) = (None, false);
|
||||
xml.maybe_read::<PropName<E>>(&mut maybe_prop, &mut dirty).await?;
|
||||
if let Some(prop) = maybe_prop {
|
||||
break PropFind::Prop(prop)
|
||||
}
|
||||
|
||||
// not found, skipping
|
||||
xml.skip().await?;
|
||||
};
|
||||
xml.close().await?;
|
||||
|
||||
Ok(propfind)
|
||||
}
|
||||
}
|
||||
|
||||
/// PROPPATCH request
|
||||
impl<E: Extension> QRead<PropertyUpdate<E>> for PropertyUpdate<E> {
|
||||
async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
|
||||
xml.open(DAV_URN, "propertyupdate").await?;
|
||||
let collected_items = xml.collect::<PropertyUpdateItem<E>>().await?;
|
||||
xml.close().await?;
|
||||
Ok(PropertyUpdate(collected_items))
|
||||
}
|
||||
}
|
||||
|
||||
/// Generic response
|
||||
impl<E: Extension> QRead<Multistatus<E>> for Multistatus<E> {
|
||||
async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
|
||||
xml.open(DAV_URN, "multistatus").await?;
|
||||
let mut responses = Vec::new();
|
||||
let mut responsedescription = None;
|
||||
|
||||
loop {
|
||||
let mut dirty = false;
|
||||
xml.maybe_push(&mut responses, &mut dirty).await?;
|
||||
xml.maybe_read(&mut responsedescription, &mut dirty).await?;
|
||||
if !dirty {
|
||||
match xml.peek() {
|
||||
Event::End(_) => break,
|
||||
_ => xml.skip().await?,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
xml.close().await?;
|
||||
Ok(Multistatus { responses, responsedescription })
|
||||
}
|
||||
}
|
||||
|
||||
// LOCK REQUEST
|
||||
impl QRead<LockInfo> for LockInfo {
|
||||
async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
|
||||
xml.open(DAV_URN, "lockinfo").await?;
|
||||
let (mut m_scope, mut m_type, mut owner) = (None, None, None);
|
||||
loop {
|
||||
let mut dirty = false;
|
||||
xml.maybe_read::<LockScope>(&mut m_scope, &mut dirty).await?;
|
||||
xml.maybe_read::<LockType>(&mut m_type, &mut dirty).await?;
|
||||
xml.maybe_read::<Owner>(&mut owner, &mut dirty).await?;
|
||||
|
||||
if !dirty {
|
||||
match xml.peek() {
|
||||
Event::End(_) => break,
|
||||
_ => xml.skip().await?,
|
||||
};
|
||||
}
|
||||
}
|
||||
xml.close().await?;
|
||||
match (m_scope, m_type) {
|
||||
(Some(lockscope), Some(locktype)) => Ok(LockInfo { lockscope, locktype, owner }),
|
||||
_ => Err(ParsingError::MissingChild),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// LOCK RESPONSE
|
||||
impl<E: Extension> QRead<PropValue<E>> for PropValue<E> {
|
||||
async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
|
||||
println!("---- propvalue");
|
||||
xml.open(DAV_URN, "prop").await?;
|
||||
let acc = xml.collect::<Property<E>>().await?;
|
||||
xml.close().await?;
|
||||
Ok(PropValue(acc))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Error response
|
||||
impl<E: Extension> QRead<Error<E>> for Error<E> {
|
||||
async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
|
||||
xml.open(DAV_URN, "error").await?;
|
||||
let violations = xml.collect::<Violation<E>>().await?;
|
||||
xml.close().await?;
|
||||
Ok(Error(violations))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ---- INNER XML
|
||||
impl<E: Extension> QRead<Response<E>> for Response<E> {
|
||||
async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
|
||||
xml.open(DAV_URN, "response").await?;
|
||||
let (mut status, mut error, mut responsedescription, mut location) = (None, None, None, None);
|
||||
let mut href = Vec::new();
|
||||
let mut propstat = Vec::new();
|
||||
|
||||
loop {
|
||||
let mut dirty = false;
|
||||
xml.maybe_read::<Status>(&mut status, &mut dirty).await?;
|
||||
xml.maybe_push::<Href>(&mut href, &mut dirty).await?;
|
||||
xml.maybe_push::<PropStat<E>>(&mut propstat, &mut dirty).await?;
|
||||
xml.maybe_read::<Error<E>>(&mut error, &mut dirty).await?;
|
||||
xml.maybe_read::<ResponseDescription>(&mut responsedescription, &mut dirty).await?;
|
||||
xml.maybe_read::<Location>(&mut location, &mut dirty).await?;
|
||||
|
||||
if !dirty {
|
||||
match xml.peek() {
|
||||
Event::End(_) => break,
|
||||
_ => { xml.skip().await? },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
xml.close().await?;
|
||||
match (status, &propstat[..], &href[..]) {
|
||||
(Some(status), &[], &[_, ..]) => Ok(Response {
|
||||
status_or_propstat: StatusOrPropstat::Status(href, status),
|
||||
error, responsedescription, location,
|
||||
}),
|
||||
(None, &[_, ..], &[_, ..]) => Ok(Response {
|
||||
status_or_propstat: StatusOrPropstat::PropStat(href.into_iter().next().unwrap(), propstat),
|
||||
error, responsedescription, location,
|
||||
}),
|
||||
(Some(_), &[_, ..], _) => Err(ParsingError::InvalidValue),
|
||||
_ => Err(ParsingError::MissingChild),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Extension> QRead<PropStat<E>> for PropStat<E> {
|
||||
async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
|
||||
xml.open(DAV_URN, "propstat").await?;
|
||||
|
||||
let (mut m_any_prop, mut m_status, mut error, mut responsedescription) = (None, None, None, None);
|
||||
|
||||
loop {
|
||||
let mut dirty = false;
|
||||
xml.maybe_read::<AnyProp<E>>(&mut m_any_prop, &mut dirty).await?;
|
||||
xml.maybe_read::<Status>(&mut m_status, &mut dirty).await?;
|
||||
xml.maybe_read::<Error<E>>(&mut error, &mut dirty).await?;
|
||||
xml.maybe_read::<ResponseDescription>(&mut responsedescription, &mut dirty).await?;
|
||||
|
||||
if !dirty {
|
||||
match xml.peek() {
|
||||
Event::End(_) => break,
|
||||
_ => xml.skip().await?,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
xml.close().await?;
|
||||
match (m_any_prop, m_status) {
|
||||
(Some(prop), Some(status)) => Ok(PropStat { prop, status, error, responsedescription }),
|
||||
_ => Err(ParsingError::MissingChild),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl QRead<Status> for Status {
|
||||
async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
|
||||
xml.open(DAV_URN, "status").await?;
|
||||
let fullcode = xml.tag_string().await?;
|
||||
let txtcode = fullcode.splitn(3, ' ').nth(1).ok_or(ParsingError::InvalidValue)?;
|
||||
let code = http::status::StatusCode::from_bytes(txtcode.as_bytes()).or(Err(ParsingError::InvalidValue))?;
|
||||
xml.close().await?;
|
||||
Ok(Status(code))
|
||||
}
|
||||
}
|
||||
|
||||
impl QRead<ResponseDescription> for ResponseDescription {
|
||||
async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
|
||||
xml.open(DAV_URN, "responsedescription").await?;
|
||||
let cnt = xml.tag_string().await?;
|
||||
xml.close().await?;
|
||||
Ok(ResponseDescription(cnt))
|
||||
}
|
||||
}
|
||||
|
||||
impl QRead<Location> for Location {
|
||||
async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
|
||||
xml.open(DAV_URN, "location").await?;
|
||||
let href = xml.find::<Href>().await?;
|
||||
xml.close().await?;
|
||||
Ok(Location(href))
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Extension> QRead<PropertyUpdateItem<E>> for PropertyUpdateItem<E> {
|
||||
async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
|
||||
match Remove::qread(xml).await {
|
||||
Err(ParsingError::Recoverable) => (),
|
||||
otherwise => return otherwise.map(PropertyUpdateItem::Remove),
|
||||
}
|
||||
Set::qread(xml).await.map(PropertyUpdateItem::Set)
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Extension> QRead<Remove<E>> for Remove<E> {
|
||||
async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
|
||||
xml.open(DAV_URN, "remove").await?;
|
||||
let propname = xml.find::<PropName<E>>().await?;
|
||||
xml.close().await?;
|
||||
Ok(Remove(propname))
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Extension> QRead<Set<E>> for Set<E> {
|
||||
async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
|
||||
xml.open(DAV_URN, "set").await?;
|
||||
let propvalue = xml.find::<PropValue<E>>().await?;
|
||||
xml.close().await?;
|
||||
Ok(Set(propvalue))
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Extension> QRead<Violation<E>> for Violation<E> {
|
||||
async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
|
||||
if xml.maybe_open(DAV_URN, "lock-token-matches-request-uri").await?.is_some() {
|
||||
xml.close().await?;
|
||||
Ok(Violation::LockTokenMatchesRequestUri)
|
||||
} else if xml.maybe_open(DAV_URN, "lock-token-submitted").await?.is_some() {
|
||||
let links = xml.collect::<Href>().await?;
|
||||
xml.close().await?;
|
||||
Ok(Violation::LockTokenSubmitted(links))
|
||||
} else if xml.maybe_open(DAV_URN, "no-conflicting-lock").await?.is_some() {
|
||||
let links = xml.collect::<Href>().await?;
|
||||
xml.close().await?;
|
||||
Ok(Violation::NoConflictingLock(links))
|
||||
} else if xml.maybe_open(DAV_URN, "no-external-entities").await?.is_some() {
|
||||
xml.close().await?;
|
||||
Ok(Violation::NoExternalEntities)
|
||||
} else if xml.maybe_open(DAV_URN, "preserved-live-properties").await?.is_some() {
|
||||
xml.close().await?;
|
||||
Ok(Violation::PreservedLiveProperties)
|
||||
} else if xml.maybe_open(DAV_URN, "propfind-finite-depth").await?.is_some() {
|
||||
xml.close().await?;
|
||||
Ok(Violation::PropfindFiniteDepth)
|
||||
} else if xml.maybe_open(DAV_URN, "cannot-modify-protected-property").await?.is_some() {
|
||||
xml.close().await?;
|
||||
Ok(Violation::CannotModifyProtectedProperty)
|
||||
} else {
|
||||
E::Error::qread(xml).await.map(Violation::Extension)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Extension> QRead<Include<E>> for Include<E> {
|
||||
async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
|
||||
xml.open(DAV_URN, "include").await?;
|
||||
let acc = xml.collect::<PropertyRequest<E>>().await?;
|
||||
xml.close().await?;
|
||||
Ok(Include(acc))
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Extension> QRead<PropName<E>> for PropName<E> {
|
||||
async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
|
||||
xml.open(DAV_URN, "prop").await?;
|
||||
let acc = xml.collect::<PropertyRequest<E>>().await?;
|
||||
xml.close().await?;
|
||||
Ok(PropName(acc))
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Extension> QRead<AnyProp<E>> for AnyProp<E> {
|
||||
async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
|
||||
xml.open(DAV_URN, "prop").await?;
|
||||
let acc = xml.collect::<AnyProperty<E>>().await?;
|
||||
xml.close().await?;
|
||||
Ok(AnyProp(acc))
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Extension> QRead<AnyProperty<E>> for AnyProperty<E> {
|
||||
async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
|
||||
match Property::qread(xml).await {
|
||||
Err(ParsingError::Recoverable) => (),
|
||||
otherwise => return otherwise.map(Self::Value)
|
||||
}
|
||||
PropertyRequest::qread(xml).await.map(Self::Request)
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Extension> QRead<PropertyRequest<E>> for PropertyRequest<E> {
|
||||
async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
|
||||
let maybe = if xml.maybe_open(DAV_URN, "creationdate").await?.is_some() {
|
||||
Some(PropertyRequest::CreationDate)
|
||||
} else if xml.maybe_open(DAV_URN, "displayname").await?.is_some() {
|
||||
Some(PropertyRequest::DisplayName)
|
||||
} else if xml.maybe_open(DAV_URN, "getcontentlanguage").await?.is_some() {
|
||||
Some(PropertyRequest::GetContentLanguage)
|
||||
} else if xml.maybe_open(DAV_URN, "getcontentlength").await?.is_some() {
|
||||
Some(PropertyRequest::GetContentLength)
|
||||
} else if xml.maybe_open(DAV_URN, "getcontenttype").await?.is_some() {
|
||||
Some(PropertyRequest::GetContentType)
|
||||
} else if xml.maybe_open(DAV_URN, "getetag").await?.is_some() {
|
||||
Some(PropertyRequest::GetEtag)
|
||||
} else if xml.maybe_open(DAV_URN, "getlastmodified").await?.is_some() {
|
||||
Some(PropertyRequest::GetLastModified)
|
||||
} else if xml.maybe_open(DAV_URN, "lockdiscovery").await?.is_some() {
|
||||
Some(PropertyRequest::LockDiscovery)
|
||||
} else if xml.maybe_open(DAV_URN, "resourcetype").await?.is_some() {
|
||||
Some(PropertyRequest::ResourceType)
|
||||
} else if xml.maybe_open(DAV_URN, "supportedlock").await?.is_some() {
|
||||
Some(PropertyRequest::SupportedLock)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
match maybe {
|
||||
Some(pr) => {
|
||||
xml.close().await?;
|
||||
Ok(pr)
|
||||
},
|
||||
None => E::PropertyRequest::qread(xml).await.map(PropertyRequest::Extension),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Extension> QRead<Property<E>> for Property<E> {
|
||||
async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
|
||||
// Core WebDAV properties
|
||||
if xml.maybe_open_start(DAV_URN, "creationdate").await?.is_some() {
|
||||
let datestr = xml.tag_string().await?;
|
||||
xml.close().await?;
|
||||
return Ok(Property::CreationDate(DateTime::parse_from_rfc3339(datestr.as_str())?))
|
||||
} else if xml.maybe_open_start(DAV_URN, "displayname").await?.is_some() {
|
||||
let name = xml.tag_string().await?;
|
||||
xml.close().await?;
|
||||
return Ok(Property::DisplayName(name))
|
||||
} else if xml.maybe_open_start(DAV_URN, "getcontentlanguage").await?.is_some() {
|
||||
let lang = xml.tag_string().await?;
|
||||
xml.close().await?;
|
||||
return Ok(Property::GetContentLanguage(lang))
|
||||
} else if xml.maybe_open_start(DAV_URN, "getcontentlength").await?.is_some() {
|
||||
let cl = xml.tag_string().await?.parse::<u64>()?;
|
||||
xml.close().await?;
|
||||
return Ok(Property::GetContentLength(cl))
|
||||
} else if xml.maybe_open_start(DAV_URN, "getcontenttype").await?.is_some() {
|
||||
let ct = xml.tag_string().await?;
|
||||
xml.close().await?;
|
||||
return Ok(Property::GetContentType(ct))
|
||||
} else if xml.maybe_open_start(DAV_URN, "getetag").await?.is_some() {
|
||||
let etag = xml.tag_string().await?;
|
||||
xml.close().await?;
|
||||
return Ok(Property::GetEtag(etag))
|
||||
} else if xml.maybe_open_start(DAV_URN, "getlastmodified").await?.is_some() {
|
||||
let datestr = xml.tag_string().await?;
|
||||
xml.close().await?;
|
||||
return Ok(Property::GetLastModified(DateTime::parse_from_rfc2822(datestr.as_str())?))
|
||||
} else if xml.maybe_open_start(DAV_URN, "lockdiscovery").await?.is_some() {
|
||||
let acc = xml.collect::<ActiveLock>().await?;
|
||||
xml.close().await?;
|
||||
return Ok(Property::LockDiscovery(acc))
|
||||
} else if xml.maybe_open_start(DAV_URN, "resourcetype").await?.is_some() {
|
||||
let acc = xml.collect::<ResourceType<E>>().await?;
|
||||
xml.close().await?;
|
||||
return Ok(Property::ResourceType(acc))
|
||||
} else if xml.maybe_open_start(DAV_URN, "supportedlock").await?.is_some() {
|
||||
let acc = xml.collect::<LockEntry>().await?;
|
||||
xml.close().await?;
|
||||
return Ok(Property::SupportedLock(acc))
|
||||
}
|
||||
|
||||
// Option 2: an extension property, delegating
|
||||
E::Property::qread(xml).await.map(Property::Extension)
|
||||
}
|
||||
}
|
||||
|
||||
impl QRead<ActiveLock> for ActiveLock {
|
||||
async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
|
||||
xml.open(DAV_URN, "activelock").await?;
|
||||
let (mut m_scope, mut m_type, mut m_depth, mut owner, mut timeout, mut locktoken, mut m_root) =
|
||||
(None, None, None, None, None, None, None);
|
||||
|
||||
loop {
|
||||
let mut dirty = false;
|
||||
xml.maybe_read::<LockScope>(&mut m_scope, &mut dirty).await?;
|
||||
xml.maybe_read::<LockType>(&mut m_type, &mut dirty).await?;
|
||||
xml.maybe_read::<Depth>(&mut m_depth, &mut dirty).await?;
|
||||
xml.maybe_read::<Owner>(&mut owner, &mut dirty).await?;
|
||||
xml.maybe_read::<Timeout>(&mut timeout, &mut dirty).await?;
|
||||
xml.maybe_read::<LockToken>(&mut locktoken, &mut dirty).await?;
|
||||
xml.maybe_read::<LockRoot>(&mut m_root, &mut dirty).await?;
|
||||
|
||||
if !dirty {
|
||||
match xml.peek() {
|
||||
Event::End(_) => break,
|
||||
_ => { xml.skip().await?; },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
xml.close().await?;
|
||||
match (m_scope, m_type, m_depth, m_root) {
|
||||
(Some(lockscope), Some(locktype), Some(depth), Some(lockroot)) =>
|
||||
Ok(ActiveLock { lockscope, locktype, depth, owner, timeout, locktoken, lockroot }),
|
||||
_ => Err(ParsingError::MissingChild),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl QRead<Depth> for Depth {
|
||||
async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
|
||||
xml.open(DAV_URN, "depth").await?;
|
||||
let depth_str = xml.tag_string().await?;
|
||||
xml.close().await?;
|
||||
match depth_str.as_str() {
|
||||
"0" => Ok(Depth::Zero),
|
||||
"1" => Ok(Depth::One),
|
||||
"infinity" => Ok(Depth::Infinity),
|
||||
_ => Err(ParsingError::WrongToken),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl QRead<Owner> for Owner {
|
||||
async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
|
||||
xml.open(DAV_URN, "owner").await?;
|
||||
|
||||
let mut owner = Owner::Unknown;
|
||||
loop {
|
||||
match xml.peek() {
|
||||
Event::Text(_) | Event::CData(_) => {
|
||||
let txt = xml.tag_string().await?;
|
||||
if matches!(owner, Owner::Unknown) {
|
||||
owner = Owner::Txt(txt);
|
||||
}
|
||||
}
|
||||
Event::Start(_) | Event::Empty(_) => {
|
||||
match Href::qread(xml).await {
|
||||
Ok(href) => { owner = Owner::Href(href); },
|
||||
Err(ParsingError::Recoverable) => { xml.skip().await?; },
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
Event::End(_) => break,
|
||||
_ => { xml.skip().await?; },
|
||||
}
|
||||
};
|
||||
xml.close().await?;
|
||||
Ok(owner)
|
||||
}
|
||||
}
|
||||
|
||||
impl QRead<Timeout> for Timeout {
|
||||
async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
|
||||
const SEC_PFX: &str = "Second-";
|
||||
xml.open(DAV_URN, "timeout").await?;
|
||||
|
||||
let timeout = match xml.tag_string().await?.as_str() {
|
||||
"Infinite" => Timeout::Infinite,
|
||||
seconds => match seconds.strip_prefix(SEC_PFX) {
|
||||
Some(secs) => Timeout::Seconds(secs.parse::<u32>()?),
|
||||
None => return Err(ParsingError::InvalidValue),
|
||||
},
|
||||
};
|
||||
|
||||
xml.close().await?;
|
||||
Ok(timeout)
|
||||
}
|
||||
}
|
||||
|
||||
impl QRead<LockToken> for LockToken {
|
||||
async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
|
||||
xml.open(DAV_URN, "locktoken").await?;
|
||||
let href = xml.find::<Href>().await?;
|
||||
xml.close().await?;
|
||||
Ok(LockToken(href))
|
||||
}
|
||||
}
|
||||
|
||||
impl QRead<LockRoot> for LockRoot {
|
||||
async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
|
||||
xml.open(DAV_URN, "lockroot").await?;
|
||||
let href = xml.find::<Href>().await?;
|
||||
xml.close().await?;
|
||||
Ok(LockRoot(href))
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Extension> QRead<ResourceType<E>> for ResourceType<E> {
|
||||
async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
|
||||
if xml.maybe_open(DAV_URN, "collection").await?.is_some() {
|
||||
xml.close().await?;
|
||||
return Ok(ResourceType::Collection)
|
||||
}
|
||||
|
||||
E::ResourceType::qread(xml).await.map(ResourceType::Extension)
|
||||
}
|
||||
}
|
||||
|
||||
impl QRead<LockEntry> for LockEntry {
|
||||
async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
|
||||
xml.open(DAV_URN, "lockentry").await?;
|
||||
let (mut maybe_scope, mut maybe_type) = (None, None);
|
||||
|
||||
loop {
|
||||
let mut dirty = false;
|
||||
xml.maybe_read::<LockScope>(&mut maybe_scope, &mut dirty).await?;
|
||||
xml.maybe_read::<LockType>(&mut maybe_type, &mut dirty).await?;
|
||||
if !dirty {
|
||||
match xml.peek() {
|
||||
Event::End(_) => break,
|
||||
_ => xml.skip().await?,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
xml.close().await?;
|
||||
match (maybe_scope, maybe_type) {
|
||||
(Some(lockscope), Some(locktype)) => Ok(LockEntry { lockscope, locktype }),
|
||||
_ => Err(ParsingError::MissingChild),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl QRead<LockScope> for LockScope {
|
||||
async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
|
||||
xml.open(DAV_URN, "lockscope").await?;
|
||||
|
||||
let lockscope = loop {
|
||||
if xml.maybe_open(DAV_URN, "exclusive").await?.is_some() {
|
||||
xml.close().await?;
|
||||
break LockScope::Exclusive
|
||||
}
|
||||
|
||||
if xml.maybe_open(DAV_URN, "shared").await?.is_some() {
|
||||
xml.close().await?;
|
||||
break LockScope::Shared
|
||||
}
|
||||
|
||||
xml.skip().await?;
|
||||
};
|
||||
|
||||
xml.close().await?;
|
||||
Ok(lockscope)
|
||||
}
|
||||
}
|
||||
|
||||
impl QRead<LockType> for LockType {
|
||||
async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
|
||||
xml.open(DAV_URN, "locktype").await?;
|
||||
|
||||
let locktype = loop {
|
||||
if xml.maybe_open(DAV_URN, "write").await?.is_some() {
|
||||
xml.close().await?;
|
||||
break LockType::Write
|
||||
}
|
||||
|
||||
xml.skip().await?;
|
||||
};
|
||||
|
||||
xml.close().await?;
|
||||
Ok(locktype)
|
||||
}
|
||||
}
|
||||
|
||||
impl QRead<Href> for Href {
|
||||
async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
|
||||
xml.open(DAV_URN, "href").await?;
|
||||
let url = xml.tag_string().await?;
|
||||
xml.close().await?;
|
||||
Ok(Href(url))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use chrono::{FixedOffset, TimeZone};
|
||||
use crate::realization::Core;
|
||||
use quick_xml::reader::NsReader;
|
||||
|
||||
#[tokio::test]
|
||||
async fn basic_propfind_propname() {
|
||||
let src = r#"<?xml version="1.0" encoding="utf-8" ?>
|
||||
<rando/>
|
||||
<garbage><old/></garbage>
|
||||
<D:propfind xmlns:D="DAV:">
|
||||
<D:propname/>
|
||||
</D:propfind>
|
||||
"#;
|
||||
|
||||
let mut rdr = Reader::new(NsReader::from_reader(src.as_bytes())).await.unwrap();
|
||||
let got = rdr.find::<PropFind::<Core>>().await.unwrap();
|
||||
|
||||
assert_eq!(got, PropFind::<Core>::PropName);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn basic_propfind_prop() {
|
||||
let src = r#"<?xml version="1.0" encoding="utf-8" ?>
|
||||
<rando/>
|
||||
<garbage><old/></garbage>
|
||||
<D:propfind xmlns:D="DAV:">
|
||||
<D:prop>
|
||||
<D:displayname/>
|
||||
<D:getcontentlength/>
|
||||
<D:getcontenttype/>
|
||||
<D:getetag/>
|
||||
<D:getlastmodified/>
|
||||
<D:resourcetype/>
|
||||
<D:supportedlock/>
|
||||
</D:prop>
|
||||
</D:propfind>
|
||||
"#;
|
||||
|
||||
let mut rdr = Reader::new(NsReader::from_reader(src.as_bytes())).await.unwrap();
|
||||
let got = rdr.find::<PropFind::<Core>>().await.unwrap();
|
||||
|
||||
assert_eq!(got, PropFind::Prop(PropName(vec![
|
||||
PropertyRequest::DisplayName,
|
||||
PropertyRequest::GetContentLength,
|
||||
PropertyRequest::GetContentType,
|
||||
PropertyRequest::GetEtag,
|
||||
PropertyRequest::GetLastModified,
|
||||
PropertyRequest::ResourceType,
|
||||
PropertyRequest::SupportedLock,
|
||||
])));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn rfc_lock_error() {
|
||||
let src = r#"<?xml version="1.0" encoding="utf-8" ?>
|
||||
<D:error xmlns:D="DAV:">
|
||||
<D:lock-token-submitted>
|
||||
<D:href>/locked/</D:href>
|
||||
</D:lock-token-submitted>
|
||||
</D:error>"#;
|
||||
|
||||
let mut rdr = Reader::new(NsReader::from_reader(src.as_bytes())).await.unwrap();
|
||||
let got = rdr.find::<Error::<Core>>().await.unwrap();
|
||||
|
||||
assert_eq!(got, Error(vec![
|
||||
Violation::LockTokenSubmitted(vec![
|
||||
Href("/locked/".into())
|
||||
])
|
||||
]));
|
||||
}
|
||||
|
||||
|
||||
#[tokio::test]
|
||||
async fn rfc_propertyupdate() {
|
||||
let src = r#"<?xml version="1.0" encoding="utf-8" ?>
|
||||
<D:propertyupdate xmlns:D="DAV:"
|
||||
xmlns:Z="http://ns.example.com/standards/z39.50/">
|
||||
<D:set>
|
||||
<D:prop>
|
||||
<Z:Authors>
|
||||
<Z:Author>Jim Whitehead</Z:Author>
|
||||
<Z:Author>Roy Fielding</Z:Author>
|
||||
</Z:Authors>
|
||||
</D:prop>
|
||||
</D:set>
|
||||
<D:remove>
|
||||
<D:prop><Z:Copyright-Owner/></D:prop>
|
||||
</D:remove>
|
||||
</D:propertyupdate>"#;
|
||||
|
||||
let mut rdr = Reader::new(NsReader::from_reader(src.as_bytes())).await.unwrap();
|
||||
let got = rdr.find::<PropertyUpdate::<Core>>().await.unwrap();
|
||||
|
||||
assert_eq!(got, PropertyUpdate(vec![
|
||||
PropertyUpdateItem::Set(Set(PropValue(vec![]))),
|
||||
PropertyUpdateItem::Remove(Remove(PropName(vec![]))),
|
||||
]));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn rfc_lockinfo() {
|
||||
let src = r#"
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<D:lockinfo xmlns:D='DAV:'>
|
||||
<D:lockscope><D:exclusive/></D:lockscope>
|
||||
<D:locktype><D:write/></D:locktype>
|
||||
<D:owner>
|
||||
<D:href>http://example.org/~ejw/contact.html</D:href>
|
||||
</D:owner>
|
||||
</D:lockinfo>
|
||||
"#;
|
||||
|
||||
let mut rdr = Reader::new(NsReader::from_reader(src.as_bytes())).await.unwrap();
|
||||
let got = rdr.find::<LockInfo>().await.unwrap();
|
||||
|
||||
assert_eq!(got, LockInfo {
|
||||
lockscope: LockScope::Exclusive,
|
||||
locktype: LockType::Write,
|
||||
owner: Some(Owner::Href(Href("http://example.org/~ejw/contact.html".into()))),
|
||||
});
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn rfc_multistatus_name() {
|
||||
let src = r#"
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<multistatus xmlns="DAV:">
|
||||
<response>
|
||||
<href>http://www.example.com/container/</href>
|
||||
<propstat>
|
||||
<prop xmlns:R="http://ns.example.com/boxschema/">
|
||||
<R:bigbox/>
|
||||
<R:author/>
|
||||
<creationdate/>
|
||||
<displayname/>
|
||||
<resourcetype/>
|
||||
<supportedlock/>
|
||||
</prop>
|
||||
<status>HTTP/1.1 200 OK</status>
|
||||
</propstat>
|
||||
</response>
|
||||
<response>
|
||||
<href>http://www.example.com/container/front.html</href>
|
||||
<propstat>
|
||||
<prop xmlns:R="http://ns.example.com/boxschema/">
|
||||
<R:bigbox/>
|
||||
<creationdate/>
|
||||
<displayname/>
|
||||
<getcontentlength/>
|
||||
<getcontenttype/>
|
||||
<getetag/>
|
||||
<getlastmodified/>
|
||||
<resourcetype/>
|
||||
<supportedlock/>
|
||||
</prop>
|
||||
<status>HTTP/1.1 200 OK</status>
|
||||
</propstat>
|
||||
</response>
|
||||
</multistatus>
|
||||
"#;
|
||||
|
||||
let mut rdr = Reader::new(NsReader::from_reader(src.as_bytes())).await.unwrap();
|
||||
let got = rdr.find::<Multistatus::<Core>>().await.unwrap();
|
||||
|
||||
assert_eq!(got, Multistatus {
|
||||
responses: vec![
|
||||
Response {
|
||||
status_or_propstat: StatusOrPropstat::PropStat(
|
||||
Href("http://www.example.com/container/".into()),
|
||||
vec![PropStat {
|
||||
prop: AnyProp(vec![
|
||||
AnyProperty::Request(PropertyRequest::CreationDate),
|
||||
AnyProperty::Request(PropertyRequest::DisplayName),
|
||||
AnyProperty::Request(PropertyRequest::ResourceType),
|
||||
AnyProperty::Request(PropertyRequest::SupportedLock),
|
||||
]),
|
||||
status: Status(http::status::StatusCode::OK),
|
||||
error: None,
|
||||
responsedescription: None,
|
||||
}],
|
||||
),
|
||||
error: None,
|
||||
responsedescription: None,
|
||||
location: None,
|
||||
},
|
||||
Response {
|
||||
status_or_propstat: StatusOrPropstat::PropStat(
|
||||
Href("http://www.example.com/container/front.html".into()),
|
||||
vec![PropStat {
|
||||
prop: AnyProp(vec![
|
||||
AnyProperty::Request(PropertyRequest::CreationDate),
|
||||
AnyProperty::Request(PropertyRequest::DisplayName),
|
||||
AnyProperty::Request(PropertyRequest::GetContentLength),
|
||||
AnyProperty::Request(PropertyRequest::GetContentType),
|
||||
AnyProperty::Request(PropertyRequest::GetEtag),
|
||||
AnyProperty::Request(PropertyRequest::GetLastModified),
|
||||
AnyProperty::Request(PropertyRequest::ResourceType),
|
||||
AnyProperty::Request(PropertyRequest::SupportedLock),
|
||||
]),
|
||||
status: Status(http::status::StatusCode::OK),
|
||||
error: None,
|
||||
responsedescription: None,
|
||||
}],
|
||||
),
|
||||
error: None,
|
||||
responsedescription: None,
|
||||
location: None,
|
||||
},
|
||||
],
|
||||
responsedescription: None,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
#[tokio::test]
|
||||
async fn rfc_multistatus_value() {
|
||||
let src = r#"
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<D:multistatus xmlns:D="DAV:">
|
||||
<D:response>
|
||||
<D:href>/container/</D:href>
|
||||
<D:propstat>
|
||||
<D:prop xmlns:R="http://ns.example.com/boxschema/">
|
||||
<R:bigbox><R:BoxType>Box type A</R:BoxType></R:bigbox>
|
||||
<R:author><R:Name>Hadrian</R:Name></R:author>
|
||||
<D:creationdate>1997-12-01T17:42:21-08:00</D:creationdate>
|
||||
<D:displayname>Example collection</D:displayname>
|
||||
<D:resourcetype><D:collection/></D:resourcetype>
|
||||
<D:supportedlock>
|
||||
<D:lockentry>
|
||||
<D:lockscope><D:exclusive/></D:lockscope>
|
||||
<D:locktype><D:write/></D:locktype>
|
||||
</D:lockentry>
|
||||
<D:lockentry>
|
||||
<D:lockscope><D:shared/></D:lockscope>
|
||||
<D:locktype><D:write/></D:locktype>
|
||||
</D:lockentry>
|
||||
</D:supportedlock>
|
||||
</D:prop>
|
||||
<D:status>HTTP/1.1 200 OK</D:status>
|
||||
</D:propstat>
|
||||
</D:response>
|
||||
<D:response>
|
||||
<D:href>/container/front.html</D:href>
|
||||
<D:propstat>
|
||||
<D:prop xmlns:R="http://ns.example.com/boxschema/">
|
||||
<R:bigbox><R:BoxType>Box type B</R:BoxType>
|
||||
</R:bigbox>
|
||||
<D:creationdate>1997-12-01T18:27:21-08:00</D:creationdate>
|
||||
<D:displayname>Example HTML resource</D:displayname>
|
||||
<D:getcontentlength>4525</D:getcontentlength>
|
||||
<D:getcontenttype>text/html</D:getcontenttype>
|
||||
<D:getetag>"zzyzx"</D:getetag>
|
||||
<D:getlastmodified
|
||||
>Mon, 12 Jan 1998 09:25:56 GMT</D:getlastmodified>
|
||||
<D:resourcetype/>
|
||||
<D:supportedlock>
|
||||
<D:lockentry>
|
||||
<D:lockscope><D:exclusive/></D:lockscope>
|
||||
<D:locktype><D:write/></D:locktype>
|
||||
</D:lockentry>
|
||||
<D:lockentry>
|
||||
<D:lockscope><D:shared/></D:lockscope>
|
||||
<D:locktype><D:write/></D:locktype>
|
||||
</D:lockentry>
|
||||
</D:supportedlock>
|
||||
</D:prop>
|
||||
<D:status>HTTP/1.1 200 OK</D:status>
|
||||
</D:propstat>
|
||||
</D:response>
|
||||
</D:multistatus>"#;
|
||||
|
||||
let mut rdr = Reader::new(NsReader::from_reader(src.as_bytes())).await.unwrap();
|
||||
let got = rdr.find::<Multistatus::<Core>>().await.unwrap();
|
||||
|
||||
assert_eq!(got, Multistatus {
|
||||
responses: vec![
|
||||
Response {
|
||||
status_or_propstat: StatusOrPropstat::PropStat(
|
||||
Href("/container/".into()),
|
||||
vec![PropStat {
|
||||
prop: AnyProp(vec![
|
||||
AnyProperty::Value(Property::CreationDate(FixedOffset::west_opt(8 * 3600).unwrap().with_ymd_and_hms(1997, 12, 01, 17, 42, 21).unwrap())),
|
||||
AnyProperty::Value(Property::DisplayName("Example collection".into())),
|
||||
AnyProperty::Value(Property::ResourceType(vec![ResourceType::Collection])),
|
||||
AnyProperty::Value(Property::SupportedLock(vec![
|
||||
LockEntry {
|
||||
lockscope: LockScope::Exclusive,
|
||||
locktype: LockType::Write,
|
||||
},
|
||||
LockEntry {
|
||||
lockscope: LockScope::Shared,
|
||||
locktype: LockType::Write,
|
||||
},
|
||||
])),
|
||||
]),
|
||||
status: Status(http::status::StatusCode::OK),
|
||||
error: None,
|
||||
responsedescription: None,
|
||||
}],
|
||||
),
|
||||
error: None,
|
||||
responsedescription: None,
|
||||
location: None,
|
||||
|
||||
},
|
||||
Response {
|
||||
status_or_propstat: StatusOrPropstat::PropStat(
|
||||
Href("/container/front.html".into()),
|
||||
vec![PropStat {
|
||||
prop: AnyProp(vec![
|
||||
AnyProperty::Value(Property::CreationDate(FixedOffset::west_opt(8 * 3600).unwrap().with_ymd_and_hms(1997, 12, 01, 18, 27, 21).unwrap())),
|
||||
AnyProperty::Value(Property::DisplayName("Example HTML resource".into())),
|
||||
AnyProperty::Value(Property::GetContentLength(4525)),
|
||||
AnyProperty::Value(Property::GetContentType("text/html".into())),
|
||||
AnyProperty::Value(Property::GetEtag(r#""zzyzx""#.into())),
|
||||
AnyProperty::Value(Property::GetLastModified(FixedOffset::west_opt(0).unwrap().with_ymd_and_hms(1998, 01, 12, 09, 25, 56).unwrap())),
|
||||
//@FIXME know bug, can't disambiguate between an empty resource
|
||||
//type value and a request resource type
|
||||
AnyProperty::Request(PropertyRequest::ResourceType),
|
||||
AnyProperty::Value(Property::SupportedLock(vec![
|
||||
LockEntry {
|
||||
lockscope: LockScope::Exclusive,
|
||||
locktype: LockType::Write,
|
||||
},
|
||||
LockEntry {
|
||||
lockscope: LockScope::Shared,
|
||||
locktype: LockType::Write,
|
||||
},
|
||||
])),
|
||||
]),
|
||||
status: Status(http::status::StatusCode::OK),
|
||||
error: None,
|
||||
responsedescription: None,
|
||||
}],
|
||||
),
|
||||
error: None,
|
||||
responsedescription: None,
|
||||
location: None,
|
||||
|
||||
},
|
||||
],
|
||||
responsedescription: None,
|
||||
});
|
||||
}
|
||||
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,62 @@
|
|||
use quick_xml::events::attributes::AttrError;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ParsingError {
|
||||
Recoverable,
|
||||
MissingChild,
|
||||
MissingAttribute,
|
||||
NamespacePrefixAlreadyUsed,
|
||||
WrongToken,
|
||||
TagNotFound,
|
||||
InvalidValue,
|
||||
Utf8Error(std::str::Utf8Error),
|
||||
QuickXml(quick_xml::Error),
|
||||
Chrono(chrono::format::ParseError),
|
||||
Int(std::num::ParseIntError),
|
||||
Eof
|
||||
}
|
||||
impl std::fmt::Display for ParsingError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Recoverable => write!(f, "Recoverable"),
|
||||
Self::MissingChild => write!(f, "Missing child"),
|
||||
Self::MissingAttribute => write!(f, "Missing attribute"),
|
||||
Self::NamespacePrefixAlreadyUsed => write!(f, "Namespace prefix already used"),
|
||||
Self::WrongToken => write!(f, "Wrong token"),
|
||||
Self::TagNotFound => write!(f, "Tag not found"),
|
||||
Self::InvalidValue => write!(f, "Invalid value"),
|
||||
Self::Utf8Error(_) => write!(f, "Utf8 Error"),
|
||||
Self::QuickXml(_) => write!(f, "Quick XML error"),
|
||||
Self::Chrono(_) => write!(f, "Chrono error"),
|
||||
Self::Int(_) => write!(f, "Number parsing error"),
|
||||
Self::Eof => write!(f, "Found EOF while expecting data"),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl std::error::Error for ParsingError {}
|
||||
impl From<AttrError> for ParsingError {
|
||||
fn from(value: AttrError) -> Self {
|
||||
Self::QuickXml(value.into())
|
||||
}
|
||||
}
|
||||
impl From<quick_xml::Error> for ParsingError {
|
||||
fn from(value: quick_xml::Error) -> Self {
|
||||
Self::QuickXml(value)
|
||||
}
|
||||
}
|
||||
impl From<std::str::Utf8Error> for ParsingError {
|
||||
fn from(value: std::str::Utf8Error) -> Self {
|
||||
Self::Utf8Error(value)
|
||||
}
|
||||
}
|
||||
impl From<chrono::format::ParseError> for ParsingError {
|
||||
fn from(value: chrono::format::ParseError) -> Self {
|
||||
Self::Chrono(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::num::ParseIntError> for ParsingError {
|
||||
fn from(value: std::num::ParseIntError) -> Self {
|
||||
Self::Int(value)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
#![feature(type_alias_impl_trait)]
|
||||
#![feature(async_closure)]
|
||||
#![feature(trait_alias)]
|
||||
|
||||
// utils
|
||||
pub mod error;
|
||||
pub mod xml;
|
||||
|
||||
// webdav
|
||||
pub mod types;
|
||||
pub mod encoder;
|
||||
pub mod decoder;
|
||||
|
||||
// calendar
|
||||
pub mod caltypes;
|
||||
pub mod calencoder;
|
||||
pub mod caldecoder;
|
||||
|
||||
// acl (wip)
|
||||
pub mod acltypes;
|
||||
pub mod aclencoder;
|
||||
pub mod acldecoder;
|
||||
|
||||
// versioning (wip)
|
||||
mod versioningtypes;
|
||||
|
||||
// final type
|
||||
pub mod realization;
|
|
@ -0,0 +1,132 @@
|
|||
use super::types as dav;
|
||||
use super::caltypes as cal;
|
||||
use super::acltypes as acl;
|
||||
use super::xml;
|
||||
use super::error;
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct Disabled(());
|
||||
impl xml::QRead<Disabled> for Disabled {
|
||||
async fn qread(_xml: &mut xml::Reader<impl xml::IRead>) -> Result<Self, error::ParsingError> {
|
||||
Err(error::ParsingError::Recoverable)
|
||||
}
|
||||
}
|
||||
impl xml::QWrite for Disabled {
|
||||
async fn qwrite(&self, _xml: &mut xml::Writer<impl xml::IWrite>) -> Result<(), quick_xml::Error> {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
/// The base WebDAV
|
||||
///
|
||||
/// Any extension is disabled through an object we can't build
|
||||
/// due to a private inner element.
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct Core {}
|
||||
impl dav::Extension for Core {
|
||||
type Error = Disabled;
|
||||
type Property = Disabled;
|
||||
type PropertyRequest = Disabled;
|
||||
type ResourceType = Disabled;
|
||||
}
|
||||
|
||||
// WebDAV with the base Calendar implementation (RFC4791)
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct Calendar {}
|
||||
impl dav::Extension for Calendar
|
||||
{
|
||||
type Error = cal::Violation;
|
||||
type Property = cal::Property;
|
||||
type PropertyRequest = cal::PropertyRequest;
|
||||
type ResourceType = cal::ResourceType;
|
||||
}
|
||||
|
||||
// ACL
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct Acl {}
|
||||
impl dav::Extension for Acl
|
||||
{
|
||||
type Error = Disabled;
|
||||
type Property = acl::Property;
|
||||
type PropertyRequest = acl::PropertyRequest;
|
||||
type ResourceType = acl::ResourceType;
|
||||
}
|
||||
|
||||
// All merged
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct All {}
|
||||
impl dav::Extension for All {
|
||||
type Error = cal::Violation;
|
||||
type Property = Property;
|
||||
type PropertyRequest = PropertyRequest;
|
||||
type ResourceType = ResourceType;
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum Property {
|
||||
Cal(cal::Property),
|
||||
Acl(acl::Property),
|
||||
}
|
||||
impl xml::QRead<Property> for Property {
|
||||
async fn qread(xml: &mut xml::Reader<impl xml::IRead>) -> Result<Self, error::ParsingError> {
|
||||
match cal::Property::qread(xml).await {
|
||||
Err(error::ParsingError::Recoverable) => (),
|
||||
otherwise => return otherwise.map(Property::Cal),
|
||||
}
|
||||
acl::Property::qread(xml).await.map(Property::Acl)
|
||||
}
|
||||
}
|
||||
impl xml::QWrite for Property {
|
||||
async fn qwrite(&self, xml: &mut xml::Writer<impl xml::IWrite>) -> Result<(), quick_xml::Error> {
|
||||
match self {
|
||||
Self::Cal(c) => c.qwrite(xml).await,
|
||||
Self::Acl(a) => a.qwrite(xml).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum PropertyRequest {
|
||||
Cal(cal::PropertyRequest),
|
||||
Acl(acl::PropertyRequest),
|
||||
}
|
||||
impl xml::QRead<PropertyRequest> for PropertyRequest {
|
||||
async fn qread(xml: &mut xml::Reader<impl xml::IRead>) -> Result<Self, error::ParsingError> {
|
||||
match cal::PropertyRequest::qread(xml).await {
|
||||
Err(error::ParsingError::Recoverable) => (),
|
||||
otherwise => return otherwise.map(PropertyRequest::Cal),
|
||||
}
|
||||
acl::PropertyRequest::qread(xml).await.map(PropertyRequest::Acl)
|
||||
}
|
||||
}
|
||||
impl xml::QWrite for PropertyRequest {
|
||||
async fn qwrite(&self, xml: &mut xml::Writer<impl xml::IWrite>) -> Result<(), quick_xml::Error> {
|
||||
match self {
|
||||
Self::Cal(c) => c.qwrite(xml).await,
|
||||
Self::Acl(a) => a.qwrite(xml).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum ResourceType {
|
||||
Cal(cal::ResourceType),
|
||||
Acl(acl::ResourceType),
|
||||
}
|
||||
impl xml::QRead<ResourceType> for ResourceType {
|
||||
async fn qread(xml: &mut xml::Reader<impl xml::IRead>) -> Result<Self, error::ParsingError> {
|
||||
match cal::ResourceType::qread(xml).await {
|
||||
Err(error::ParsingError::Recoverable) => (),
|
||||
otherwise => return otherwise.map(ResourceType::Cal),
|
||||
}
|
||||
acl::ResourceType::qread(xml).await.map(ResourceType::Acl)
|
||||
}
|
||||
}
|
||||
impl xml::QWrite for ResourceType {
|
||||
async fn qwrite(&self, xml: &mut xml::Writer<impl xml::IWrite>) -> Result<(), quick_xml::Error> {
|
||||
match self {
|
||||
Self::Cal(c) => c.qwrite(xml).await,
|
||||
Self::Acl(a) => a.qwrite(xml).await,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,963 @@
|
|||
#![allow(dead_code)]
|
||||
use std::fmt::Debug;
|
||||
|
||||
use chrono::{DateTime,FixedOffset};
|
||||
use super::xml;
|
||||
|
||||
/// It's how we implement a DAV extension
|
||||
/// (That's the dark magic part...)
|
||||
pub trait Extension: std::fmt::Debug + PartialEq + Clone {
|
||||
type Error: xml::Node<Self::Error>;
|
||||
type Property: xml::Node<Self::Property>;
|
||||
type PropertyRequest: xml::Node<Self::PropertyRequest>;
|
||||
type ResourceType: xml::Node<Self::ResourceType>;
|
||||
}
|
||||
|
||||
/// 14.1. activelock XML Element
|
||||
///
|
||||
/// Name: activelock
|
||||
///
|
||||
/// Purpose: Describes a lock on a resource.
|
||||
/// <!ELEMENT activelock (lockscope, locktype, depth, owner?, timeout?,
|
||||
/// locktoken?, lockroot)>
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct ActiveLock {
|
||||
pub lockscope: LockScope,
|
||||
pub locktype: LockType,
|
||||
pub depth: Depth,
|
||||
pub owner: Option<Owner>,
|
||||
pub timeout: Option<Timeout>,
|
||||
pub locktoken: Option<LockToken>,
|
||||
pub lockroot: LockRoot,
|
||||
}
|
||||
|
||||
/// 14.3 collection XML Element
|
||||
///
|
||||
/// Name: collection
|
||||
///
|
||||
/// Purpose: Identifies the associated resource as a collection. The
|
||||
/// DAV:resourcetype property of a collection resource MUST contain
|
||||
/// this element. It is normally empty but extensions may add sub-
|
||||
/// elements.
|
||||
///
|
||||
/// <!ELEMENT collection EMPTY >
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct Collection{}
|
||||
|
||||
/// 14.4 depth XML Element
|
||||
///
|
||||
/// Name: depth
|
||||
///
|
||||
/// Purpose: Used for representing depth values in XML content (e.g.,
|
||||
/// in lock information).
|
||||
///
|
||||
/// Value: "0" | "1" | "infinity"
|
||||
///
|
||||
/// <!ELEMENT depth (#PCDATA) >
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum Depth {
|
||||
Zero,
|
||||
One,
|
||||
Infinity
|
||||
}
|
||||
|
||||
/// 14.5 error XML Element
|
||||
///
|
||||
/// Name: error
|
||||
///
|
||||
/// Purpose: Error responses, particularly 403 Forbidden and 409
|
||||
/// Conflict, sometimes need more information to indicate what went
|
||||
/// wrong. In these cases, servers MAY return an XML response body
|
||||
/// with a document element of 'error', containing child elements
|
||||
/// identifying particular condition codes.
|
||||
///
|
||||
/// Description: Contains at least one XML element, and MUST NOT
|
||||
/// contain text or mixed content. Any element that is a child of the
|
||||
/// 'error' element is considered to be a precondition or
|
||||
/// postcondition code. Unrecognized elements MUST be ignored.
|
||||
///
|
||||
/// <!ELEMENT error ANY >
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct Error<E: Extension>(pub Vec<Violation<E>>);
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum Violation<E: Extension> {
|
||||
/// Name: lock-token-matches-request-uri
|
||||
///
|
||||
/// Use with: 409 Conflict
|
||||
///
|
||||
/// Purpose: (precondition) -- A request may include a Lock-Token header
|
||||
/// to identify a lock for the UNLOCK method. However, if the
|
||||
/// Request-URI does not fall within the scope of the lock identified
|
||||
/// by the token, the server SHOULD use this error. The lock may have
|
||||
/// a scope that does not include the Request-URI, or the lock could
|
||||
/// have disappeared, or the token may be invalid.
|
||||
LockTokenMatchesRequestUri,
|
||||
|
||||
/// Name: lock-token-submitted (precondition)
|
||||
///
|
||||
/// Use with: 423 Locked
|
||||
///
|
||||
/// Purpose: The request could not succeed because a lock token should
|
||||
/// have been submitted. This element, if present, MUST contain at
|
||||
/// least one URL of a locked resource that prevented the request. In
|
||||
/// cases of MOVE, COPY, and DELETE where collection locks are
|
||||
/// involved, it can be difficult for the client to find out which
|
||||
/// locked resource made the request fail -- but the server is only
|
||||
/// responsible for returning one such locked resource. The server
|
||||
/// MAY return every locked resource that prevented the request from
|
||||
/// succeeding if it knows them all.
|
||||
///
|
||||
/// <!ELEMENT lock-token-submitted (href+) >
|
||||
LockTokenSubmitted(Vec<Href>),
|
||||
|
||||
/// Name: no-conflicting-lock (precondition)
|
||||
///
|
||||
/// Use with: Typically 423 Locked
|
||||
///
|
||||
/// Purpose: A LOCK request failed due the presence of an already
|
||||
/// existing conflicting lock. Note that a lock can be in conflict
|
||||
/// although the resource to which the request was directed is only
|
||||
/// indirectly locked. In this case, the precondition code can be
|
||||
/// used to inform the client about the resource that is the root of
|
||||
/// the conflicting lock, avoiding a separate lookup of the
|
||||
/// "lockdiscovery" property.
|
||||
///
|
||||
/// <!ELEMENT no-conflicting-lock (href)* >
|
||||
NoConflictingLock(Vec<Href>),
|
||||
|
||||
/// Name: no-external-entities
|
||||
///
|
||||
/// Use with: 403 Forbidden
|
||||
///
|
||||
/// Purpose: (precondition) -- If the server rejects a client request
|
||||
/// because the request body contains an external entity, the server
|
||||
/// SHOULD use this error.
|
||||
NoExternalEntities,
|
||||
|
||||
/// Name: preserved-live-properties
|
||||
///
|
||||
/// Use with: 409 Conflict
|
||||
///
|
||||
/// Purpose: (postcondition) -- The server received an otherwise-valid
|
||||
/// MOVE or COPY request, but cannot maintain the live properties with
|
||||
/// the same behavior at the destination. It may be that the server
|
||||
/// only supports some live properties in some parts of the
|
||||
/// repository, or simply has an internal error.
|
||||
PreservedLiveProperties,
|
||||
|
||||
/// Name: propfind-finite-depth
|
||||
///
|
||||
/// Use with: 403 Forbidden
|
||||
///
|
||||
/// Purpose: (precondition) -- This server does not allow infinite-depth
|
||||
/// PROPFIND requests on collections.
|
||||
PropfindFiniteDepth,
|
||||
|
||||
|
||||
/// Name: cannot-modify-protected-property
|
||||
///
|
||||
/// Use with: 403 Forbidden
|
||||
///
|
||||
/// Purpose: (precondition) -- The client attempted to set a protected
|
||||
/// property in a PROPPATCH (such as DAV:getetag). See also
|
||||
/// [RFC3253], Section 3.12.
|
||||
CannotModifyProtectedProperty,
|
||||
|
||||
/// Specific errors
|
||||
Extension(E::Error),
|
||||
}
|
||||
|
||||
/// 14.6. exclusive XML Element
|
||||
///
|
||||
/// Name: exclusive
|
||||
///
|
||||
/// Purpose: Specifies an exclusive lock.
|
||||
///
|
||||
/// <!ELEMENT exclusive EMPTY >
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct Exclusive {}
|
||||
|
||||
/// 14.7. href XML Element
|
||||
///
|
||||
/// Name: href
|
||||
///
|
||||
/// Purpose: MUST contain a URI or a relative reference.
|
||||
///
|
||||
/// Description: There may be limits on the value of 'href' depending
|
||||
/// on the context of its use. Refer to the specification text where
|
||||
/// 'href' is used to see what limitations apply in each case.
|
||||
///
|
||||
/// Value: Simple-ref
|
||||
///
|
||||
/// <!ELEMENT href (#PCDATA)>
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct Href(pub String);
|
||||
|
||||
|
||||
/// 14.8. include XML Element
|
||||
///
|
||||
/// Name: include
|
||||
///
|
||||
/// Purpose: Any child element represents the name of a property to be
|
||||
/// included in the PROPFIND response. All elements inside an
|
||||
/// 'include' XML element MUST define properties related to the
|
||||
/// resource, although possible property names are in no way limited
|
||||
/// to those property names defined in this document or other
|
||||
/// standards. This element MUST NOT contain text or mixed content.
|
||||
///
|
||||
/// <!ELEMENT include ANY >
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct Include<E: Extension>(pub Vec<PropertyRequest<E>>);
|
||||
|
||||
/// 14.9. location XML Element
|
||||
///
|
||||
/// Name: location
|
||||
///
|
||||
/// Purpose: HTTP defines the "Location" header (see [RFC2616], Section
|
||||
/// 14.30) for use with some status codes (such as 201 and the 300
|
||||
/// series codes). When these codes are used inside a 'multistatus'
|
||||
/// element, the 'location' element can be used to provide the
|
||||
/// accompanying Location header value.
|
||||
///
|
||||
/// Description: Contains a single href element with the same value
|
||||
/// that would be used in a Location header.
|
||||
///
|
||||
/// <!ELEMENT location (href)>
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct Location(pub Href);
|
||||
|
||||
/// 14.10. lockentry XML Element
|
||||
///
|
||||
/// Name: lockentry
|
||||
///
|
||||
/// Purpose: Defines the types of locks that can be used with the
|
||||
/// resource.
|
||||
///
|
||||
/// <!ELEMENT lockentry (lockscope, locktype) >
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct LockEntry {
|
||||
pub lockscope: LockScope,
|
||||
pub locktype: LockType,
|
||||
}
|
||||
|
||||
/// 14.11. lockinfo XML Element
|
||||
///
|
||||
/// Name: lockinfo
|
||||
///
|
||||
/// Purpose: The 'lockinfo' XML element is used with a LOCK method to
|
||||
/// specify the type of lock the client wishes to have created.
|
||||
///
|
||||
/// <!ELEMENT lockinfo (lockscope, locktype, owner?) >
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct LockInfo {
|
||||
pub lockscope: LockScope,
|
||||
pub locktype: LockType,
|
||||
pub owner: Option<Owner>,
|
||||
}
|
||||
|
||||
/// 14.12. lockroot XML Element
|
||||
///
|
||||
/// Name: lockroot
|
||||
///
|
||||
/// Purpose: Contains the root URL of the lock, which is the URL
|
||||
/// through which the resource was addressed in the LOCK request.
|
||||
///
|
||||
/// Description: The href element contains the root of the lock. The
|
||||
/// server SHOULD include this in all DAV:lockdiscovery property
|
||||
/// values and the response to LOCK requests.
|
||||
///
|
||||
/// <!ELEMENT lockroot (href) >
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct LockRoot(pub Href);
|
||||
|
||||
/// 14.13. lockscope XML Element
|
||||
///
|
||||
/// Name: lockscope
|
||||
///
|
||||
/// Purpose: Specifies whether a lock is an exclusive lock, or a shared
|
||||
/// lock.
|
||||
/// <!ELEMENT lockscope (exclusive | shared) >
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum LockScope {
|
||||
Exclusive,
|
||||
Shared
|
||||
}
|
||||
|
||||
/// 14.14. locktoken XML Element
|
||||
///
|
||||
/// Name: locktoken
|
||||
///
|
||||
/// Purpose: The lock token associated with a lock.
|
||||
///
|
||||
/// Description: The href contains a single lock token URI, which
|
||||
/// refers to the lock.
|
||||
///
|
||||
/// <!ELEMENT locktoken (href) >
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct LockToken(pub Href);
|
||||
|
||||
/// 14.15. locktype XML Element
|
||||
///
|
||||
/// Name: locktype
|
||||
///
|
||||
/// Purpose: Specifies the access type of a lock. At present, this
|
||||
/// specification only defines one lock type, the write lock.
|
||||
///
|
||||
/// <!ELEMENT locktype (write) >
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum LockType {
|
||||
/// 14.30. write XML Element
|
||||
///
|
||||
/// Name: write
|
||||
///
|
||||
/// Purpose: Specifies a write lock.
|
||||
///
|
||||
///
|
||||
/// <!ELEMENT write EMPTY >
|
||||
Write
|
||||
}
|
||||
|
||||
/// 14.16. multistatus XML Element
|
||||
///
|
||||
/// Name: multistatus
|
||||
///
|
||||
/// Purpose: Contains multiple response messages.
|
||||
///
|
||||
/// Description: The 'responsedescription' element at the top level is
|
||||
/// used to provide a general message describing the overarching
|
||||
/// nature of the response. If this value is available, an
|
||||
/// application may use it instead of presenting the individual
|
||||
/// response descriptions contained within the responses.
|
||||
///
|
||||
/// <!ELEMENT multistatus (response*, responsedescription?) >
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct Multistatus<E: Extension> {
|
||||
pub responses: Vec<Response<E>>,
|
||||
pub responsedescription: Option<ResponseDescription>,
|
||||
}
|
||||
|
||||
/// 14.17. owner XML Element
|
||||
///
|
||||
/// Name: owner
|
||||
///
|
||||
/// Purpose: Holds client-supplied information about the creator of a
|
||||
/// lock.
|
||||
///
|
||||
/// Description: Allows a client to provide information sufficient for
|
||||
/// either directly contacting a principal (such as a telephone number
|
||||
/// or Email URI), or for discovering the principal (such as the URL
|
||||
/// of a homepage) who created a lock. The value provided MUST be
|
||||
/// treated as a dead property in terms of XML Information Item
|
||||
/// preservation. The server MUST NOT alter the value unless the
|
||||
/// owner value provided by the client is empty. For a certain amount
|
||||
/// of interoperability between different client implementations, if
|
||||
/// clients have URI-formatted contact information for the lock
|
||||
/// creator suitable for user display, then clients SHOULD put those
|
||||
/// URIs in 'href' child elements of the 'owner' element.
|
||||
///
|
||||
/// Extensibility: MAY be extended with child elements, mixed content,
|
||||
/// text content or attributes.
|
||||
///
|
||||
/// <!ELEMENT owner ANY >
|
||||
//@FIXME might need support for an extension
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum Owner {
|
||||
Txt(String),
|
||||
Href(Href),
|
||||
Unknown,
|
||||
}
|
||||
|
||||
/// 14.18. prop XML Element
|
||||
///
|
||||
/// Name: prop
|
||||
///
|
||||
/// Purpose: Contains properties related to a resource.
|
||||
///
|
||||
/// Description: A generic container for properties defined on
|
||||
/// resources. All elements inside a 'prop' XML element MUST define
|
||||
/// properties related to the resource, although possible property
|
||||
/// names are in no way limited to those property names defined in
|
||||
/// this document or other standards. This element MUST NOT contain
|
||||
/// text or mixed content.
|
||||
///
|
||||
/// <!ELEMENT prop ANY >
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct PropName<E: Extension>(pub Vec<PropertyRequest<E>>);
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct PropValue<E: Extension>(pub Vec<Property<E>>);
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct AnyProp<E: Extension>(pub Vec<AnyProperty<E>>);
|
||||
|
||||
/// 14.19. propertyupdate XML Element
|
||||
///
|
||||
/// Name: propertyupdate
|
||||
///
|
||||
/// Purpose: Contains a request to alter the properties on a resource.
|
||||
///
|
||||
/// Description: This XML element is a container for the information
|
||||
/// required to modify the properties on the resource.
|
||||
///
|
||||
/// <!ELEMENT propertyupdate (remove | set)+ >
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct PropertyUpdate<E: Extension>(pub Vec<PropertyUpdateItem<E>>);
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum PropertyUpdateItem<E: Extension> {
|
||||
Remove(Remove<E>),
|
||||
Set(Set<E>),
|
||||
}
|
||||
|
||||
/// 14.2 allprop XML Element
|
||||
///
|
||||
/// Name: allprop
|
||||
///
|
||||
/// Purpose: Specifies that all names and values of dead properties and
|
||||
/// the live properties defined by this document existing on the
|
||||
/// resource are to be returned.
|
||||
///
|
||||
/// <!ELEMENT allprop EMPTY >
|
||||
///
|
||||
/// ---
|
||||
///
|
||||
/// 14.21. propname XML Element
|
||||
///
|
||||
/// Name: propname
|
||||
///
|
||||
/// Purpose: Specifies that only a list of property names on the
|
||||
/// resource is to be returned.
|
||||
///
|
||||
/// <!ELEMENT propname EMPTY >
|
||||
///
|
||||
/// ---
|
||||
///
|
||||
/// 14.20. propfind XML Element
|
||||
///
|
||||
/// Name: propfind
|
||||
///
|
||||
/// Purpose: Specifies the properties to be returned from a PROPFIND
|
||||
/// method. Four special elements are specified for use with
|
||||
/// 'propfind': 'prop', 'allprop', 'include', and 'propname'. If
|
||||
/// 'prop' is used inside 'propfind', it MUST NOT contain property
|
||||
/// values.
|
||||
///
|
||||
/// <!ELEMENT propfind ( propname | (allprop, include?) | prop ) >
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum PropFind<E: Extension> {
|
||||
PropName,
|
||||
AllProp(Option<Include<E>>),
|
||||
Prop(PropName<E>),
|
||||
}
|
||||
|
||||
/// 14.22 propstat XML Element
|
||||
///
|
||||
/// Name: propstat
|
||||
///
|
||||
/// Purpose: Groups together a prop and status element that is
|
||||
/// associated with a particular 'href' element.
|
||||
///
|
||||
/// Description: The propstat XML element MUST contain one prop XML
|
||||
/// element and one status XML element. The contents of the prop XML
|
||||
/// element MUST only list the names of properties to which the result
|
||||
/// in the status element applies. The optional precondition/
|
||||
/// postcondition element and 'responsedescription' text also apply to
|
||||
/// the properties named in 'prop'.
|
||||
///
|
||||
/// <!ELEMENT propstat (prop, status, error?, responsedescription?) >
|
||||
///
|
||||
/// ---
|
||||
///
|
||||
///
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct PropStat<E: Extension> {
|
||||
pub prop: AnyProp<E>,
|
||||
pub status: Status,
|
||||
pub error: Option<Error<E>>,
|
||||
pub responsedescription: Option<ResponseDescription>,
|
||||
}
|
||||
|
||||
|
||||
/// 14.23. remove XML Element
|
||||
///
|
||||
/// Name: remove
|
||||
///
|
||||
/// Purpose: Lists the properties to be removed from a resource.
|
||||
///
|
||||
/// Description: Remove instructs that the properties specified in prop
|
||||
/// should be removed. Specifying the removal of a property that does
|
||||
/// not exist is not an error. All the XML elements in a 'prop' XML
|
||||
/// element inside of a 'remove' XML element MUST be empty, as only
|
||||
/// the names of properties to be removed are required.
|
||||
///
|
||||
/// <!ELEMENT remove (prop) >
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct Remove<E: Extension>(pub PropName<E>);
|
||||
|
||||
/// 14.24. response XML Element
|
||||
///
|
||||
/// Name: response
|
||||
///
|
||||
/// Purpose: Holds a single response describing the effect of a method
|
||||
/// on resource and/or its properties.
|
||||
///
|
||||
/// Description: The 'href' element contains an HTTP URL pointing to a
|
||||
/// WebDAV resource when used in the 'response' container. A
|
||||
/// particular 'href' value MUST NOT appear more than once as the
|
||||
/// child of a 'response' XML element under a 'multistatus' XML
|
||||
/// element. This requirement is necessary in order to keep
|
||||
/// processing costs for a response to linear time. Essentially, this
|
||||
/// prevents having to search in order to group together all the
|
||||
/// responses by 'href'. There are, however, no requirements
|
||||
/// regarding ordering based on 'href' values. The optional
|
||||
/// precondition/postcondition element and 'responsedescription' text
|
||||
/// can provide additional information about this resource relative to
|
||||
/// the request or result.
|
||||
///
|
||||
/// <!ELEMENT response (href, ((href*, status)|(propstat+)),
|
||||
/// error?, responsedescription? , location?) >
|
||||
///
|
||||
/// --- rewritten as ---
|
||||
/// <!ELEMENT response ((href+, status)|(href, propstat+), error?, responsedescription?, location?>
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum StatusOrPropstat<E: Extension> {
|
||||
// One status, multiple hrefs...
|
||||
Status(Vec<Href>, Status),
|
||||
// A single href, multiple properties...
|
||||
PropStat(Href, Vec<PropStat<E>>),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct Response<E: Extension> {
|
||||
pub status_or_propstat: StatusOrPropstat<E>,
|
||||
pub error: Option<Error<E>>,
|
||||
pub responsedescription: Option<ResponseDescription>,
|
||||
pub location: Option<Location>,
|
||||
}
|
||||
|
||||
/// 14.25. responsedescription XML Element
|
||||
///
|
||||
/// Name: responsedescription
|
||||
///
|
||||
/// Purpose: Contains information about a status response within a
|
||||
/// Multi-Status.
|
||||
///
|
||||
/// Description: Provides information suitable to be presented to a
|
||||
/// user.
|
||||
///
|
||||
/// <!ELEMENT responsedescription (#PCDATA) >
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct ResponseDescription(pub String);
|
||||
|
||||
/// 14.26. set XML Element
|
||||
///
|
||||
/// Name: set
|
||||
///
|
||||
/// Purpose: Lists the property values to be set for a resource.
|
||||
///
|
||||
/// Description: The 'set' element MUST contain only a 'prop' element.
|
||||
/// The elements contained by the 'prop' element inside the 'set'
|
||||
/// element MUST specify the name and value of properties that are set
|
||||
/// on the resource identified by Request-URI. If a property already
|
||||
/// exists, then its value is replaced. Language tagging information
|
||||
/// appearing in the scope of the 'prop' element (in the "xml:lang"
|
||||
/// attribute, if present) MUST be persistently stored along with the
|
||||
/// property, and MUST be subsequently retrievable using PROPFIND.
|
||||
///
|
||||
/// <!ELEMENT set (prop) >
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct Set<E: Extension>(pub PropValue<E>);
|
||||
|
||||
/// 14.27. shared XML Element
|
||||
///
|
||||
/// Name: shared
|
||||
///
|
||||
/// Purpose: Specifies a shared lock.
|
||||
///
|
||||
///
|
||||
/// <!ELEMENT shared EMPTY >
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct Shared {}
|
||||
|
||||
|
||||
/// 14.28. status XML Element
|
||||
///
|
||||
/// Name: status
|
||||
///
|
||||
/// Purpose: Holds a single HTTP status-line.
|
||||
///
|
||||
/// Value: status-line (defined in Section 6.1 of [RFC2616])
|
||||
///
|
||||
/// <!ELEMENT status (#PCDATA) >
|
||||
//@FIXME: Better typing is possible with an enum for example
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct Status(pub http::status::StatusCode);
|
||||
|
||||
/// 14.29. timeout XML Element
|
||||
///
|
||||
/// Name: timeout
|
||||
///
|
||||
/// Purpose: The number of seconds remaining before a lock expires.
|
||||
///
|
||||
/// Value: TimeType (defined in Section 10.7)
|
||||
///
|
||||
///
|
||||
/// <!ELEMENT timeout (#PCDATA) >
|
||||
///
|
||||
/// TimeOut = "Timeout" ":" 1#TimeType
|
||||
/// TimeType = ("Second-" DAVTimeOutVal | "Infinite")
|
||||
/// ; No LWS allowed within TimeType
|
||||
/// DAVTimeOutVal = 1*DIGIT
|
||||
///
|
||||
/// Clients MAY include Timeout request headers in their LOCK requests.
|
||||
/// However, the server is not required to honor or even consider these
|
||||
/// requests. Clients MUST NOT submit a Timeout request header with any
|
||||
/// method other than a LOCK method.
|
||||
///
|
||||
/// The "Second" TimeType specifies the number of seconds that will
|
||||
/// elapse between granting of the lock at the server, and the automatic
|
||||
/// removal of the lock. The timeout value for TimeType "Second" MUST
|
||||
/// NOT be greater than 2^32-1.
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum Timeout {
|
||||
Seconds(u32),
|
||||
Infinite,
|
||||
}
|
||||
|
||||
|
||||
/// 15. DAV Properties
|
||||
///
|
||||
/// For DAV properties, the name of the property is also the same as the
|
||||
/// name of the XML element that contains its value. In the section
|
||||
/// below, the final line of each section gives the element type
|
||||
/// declaration using the format defined in [REC-XML]. The "Value"
|
||||
/// field, where present, specifies further restrictions on the allowable
|
||||
/// contents of the XML element using BNF (i.e., to further restrict the
|
||||
/// values of a PCDATA element).
|
||||
///
|
||||
/// A protected property is one that cannot be changed with a PROPPATCH
|
||||
/// request. There may be other requests that would result in a change
|
||||
/// to a protected property (as when a LOCK request affects the value of
|
||||
/// DAV:lockdiscovery). Note that a given property could be protected on
|
||||
/// one type of resource, but not protected on another type of resource.
|
||||
///
|
||||
/// A computed property is one with a value defined in terms of a
|
||||
/// computation (based on the content and other properties of that
|
||||
/// resource, or even of some other resource). A computed property is
|
||||
/// always a protected property.
|
||||
///
|
||||
/// COPY and MOVE behavior refers to local COPY and MOVE operations.
|
||||
///
|
||||
/// For properties defined based on HTTP GET response headers (DAV:get*),
|
||||
/// the header value could include LWS as defined in [RFC2616], Section
|
||||
/// 4.2. Server implementors SHOULD strip LWS from these values before
|
||||
/// using as WebDAV property values.
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum AnyProperty<E: Extension> {
|
||||
Request(PropertyRequest<E>),
|
||||
Value(Property<E>),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum PropertyRequest<E: Extension> {
|
||||
CreationDate,
|
||||
DisplayName,
|
||||
GetContentLanguage,
|
||||
GetContentLength,
|
||||
GetContentType,
|
||||
GetEtag,
|
||||
GetLastModified,
|
||||
LockDiscovery,
|
||||
ResourceType,
|
||||
SupportedLock,
|
||||
Extension(E::PropertyRequest),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum Property<E: Extension> {
|
||||
/// 15.1. creationdate Property
|
||||
///
|
||||
/// Name: creationdate
|
||||
///
|
||||
/// Purpose: Records the time and date the resource was created.
|
||||
///
|
||||
/// Value: date-time (defined in [RFC3339], see the ABNF in Section
|
||||
/// 5.6.)
|
||||
///
|
||||
/// Protected: MAY be protected. Some servers allow DAV:creationdate
|
||||
/// to be changed to reflect the time the document was created if that
|
||||
/// is more meaningful to the user (rather than the time it was
|
||||
/// uploaded). Thus, clients SHOULD NOT use this property in
|
||||
/// synchronization logic (use DAV:getetag instead).
|
||||
///
|
||||
/// COPY/MOVE behavior: This property value SHOULD be kept during a
|
||||
/// MOVE operation, but is normally re-initialized when a resource is
|
||||
/// created with a COPY. It should not be set in a COPY.
|
||||
///
|
||||
/// Description: The DAV:creationdate property SHOULD be defined on all
|
||||
/// DAV compliant resources. If present, it contains a timestamp of
|
||||
/// the moment when the resource was created. Servers that are
|
||||
/// incapable of persistently recording the creation date SHOULD
|
||||
/// instead leave it undefined (i.e. report "Not Found").
|
||||
///
|
||||
/// <!ELEMENT creationdate (#PCDATA) >
|
||||
CreationDate(DateTime<FixedOffset>),
|
||||
|
||||
/// 15.2. displayname Property
|
||||
///
|
||||
/// Name: displayname
|
||||
///
|
||||
/// Purpose: Provides a name for the resource that is suitable for
|
||||
/// presentation to a user.
|
||||
///
|
||||
/// Value: Any text.
|
||||
///
|
||||
/// Protected: SHOULD NOT be protected. Note that servers implementing
|
||||
/// [RFC2518] might have made this a protected property as this is a
|
||||
/// new requirement.
|
||||
///
|
||||
/// COPY/MOVE behavior: This property value SHOULD be preserved in COPY
|
||||
/// and MOVE operations.
|
||||
///
|
||||
/// Description: Contains a description of the resource that is
|
||||
/// suitable for presentation to a user. This property is defined on
|
||||
/// the resource, and hence SHOULD have the same value independent of
|
||||
/// the Request-URI used to retrieve it (thus, computing this property
|
||||
/// based on the Request-URI is deprecated). While generic clients
|
||||
/// might display the property value to end users, client UI designers
|
||||
/// must understand that the method for identifying resources is still
|
||||
/// the URL. Changes to DAV:displayname do not issue moves or copies
|
||||
/// to the server, but simply change a piece of meta-data on the
|
||||
/// individual resource. Two resources can have the same DAV:
|
||||
/// displayname value even within the same collection.
|
||||
///
|
||||
/// <!ELEMENT displayname (#PCDATA) >
|
||||
DisplayName(String),
|
||||
|
||||
|
||||
/// 15.3. getcontentlanguage Property
|
||||
///
|
||||
/// Name: getcontentlanguage
|
||||
///
|
||||
/// Purpose: Contains the Content-Language header value (from Section
|
||||
/// 14.12 of [RFC2616]) as it would be returned by a GET without
|
||||
/// accept headers.
|
||||
///
|
||||
/// Value: language-tag (language-tag is defined in Section 3.10 of
|
||||
/// [RFC2616])
|
||||
///
|
||||
/// Protected: SHOULD NOT be protected, so that clients can reset the
|
||||
/// language. Note that servers implementing [RFC2518] might have
|
||||
/// made this a protected property as this is a new requirement.
|
||||
///
|
||||
/// COPY/MOVE behavior: This property value SHOULD be preserved in COPY
|
||||
/// and MOVE operations.
|
||||
///
|
||||
/// Description: The DAV:getcontentlanguage property MUST be defined on
|
||||
/// any DAV-compliant resource that returns the Content-Language
|
||||
/// header on a GET.
|
||||
///
|
||||
/// <!ELEMENT getcontentlanguage (#PCDATA) >
|
||||
GetContentLanguage(String),
|
||||
|
||||
/// 15.4. getcontentlength Property
|
||||
///
|
||||
/// Name: getcontentlength
|
||||
///
|
||||
/// Purpose: Contains the Content-Length header returned by a GET
|
||||
/// without accept headers.
|
||||
///
|
||||
/// Value: See Section 14.13 of [RFC2616].
|
||||
///
|
||||
/// Protected: This property is computed, therefore protected.
|
||||
///
|
||||
/// Description: The DAV:getcontentlength property MUST be defined on
|
||||
/// any DAV-compliant resource that returns the Content-Length header
|
||||
/// in response to a GET.
|
||||
///
|
||||
/// COPY/MOVE behavior: This property value is dependent on the size of
|
||||
/// the destination resource, not the value of the property on the
|
||||
/// source resource.
|
||||
///
|
||||
/// <!ELEMENT getcontentlength (#PCDATA) >
|
||||
GetContentLength(u64),
|
||||
|
||||
/// 15.5. getcontenttype Property
|
||||
///
|
||||
/// Name: getcontenttype
|
||||
///
|
||||
/// Purpose: Contains the Content-Type header value (from Section 14.17
|
||||
/// of [RFC2616]) as it would be returned by a GET without accept
|
||||
/// headers.
|
||||
///
|
||||
/// Value: media-type (defined in Section 3.7 of [RFC2616])
|
||||
///
|
||||
/// Protected: Potentially protected if the server prefers to assign
|
||||
/// content types on its own (see also discussion in Section 9.7.1).
|
||||
///
|
||||
/// COPY/MOVE behavior: This property value SHOULD be preserved in COPY
|
||||
/// and MOVE operations.
|
||||
///
|
||||
/// Description: This property MUST be defined on any DAV-compliant
|
||||
/// resource that returns the Content-Type header in response to a
|
||||
/// GET.
|
||||
///
|
||||
/// <!ELEMENT getcontenttype (#PCDATA) >
|
||||
GetContentType(String),
|
||||
|
||||
/// 15.6. getetag Property
|
||||
///
|
||||
/// Name: getetag
|
||||
///
|
||||
/// Purpose: Contains the ETag header value (from Section 14.19 of
|
||||
/// [RFC2616]) as it would be returned by a GET without accept
|
||||
/// headers.
|
||||
///
|
||||
/// Value: entity-tag (defined in Section 3.11 of [RFC2616])
|
||||
///
|
||||
/// Protected: MUST be protected because this value is created and
|
||||
/// controlled by the server.
|
||||
///
|
||||
/// COPY/MOVE behavior: This property value is dependent on the final
|
||||
/// state of the destination resource, not the value of the property
|
||||
/// on the source resource. Also note the considerations in
|
||||
/// Section 8.8.
|
||||
///
|
||||
/// Description: The getetag property MUST be defined on any DAV-
|
||||
/// compliant resource that returns the Etag header. Refer to Section
|
||||
/// 3.11 of RFC 2616 for a complete definition of the semantics of an
|
||||
/// ETag, and to Section 8.6 for a discussion of ETags in WebDAV.
|
||||
///
|
||||
/// <!ELEMENT getetag (#PCDATA) >
|
||||
GetEtag(String),
|
||||
|
||||
/// 15.7. getlastmodified Property
|
||||
///
|
||||
/// Name: getlastmodified
|
||||
///
|
||||
/// Purpose: Contains the Last-Modified header value (from Section
|
||||
/// 14.29 of [RFC2616]) as it would be returned by a GET method
|
||||
/// without accept headers.
|
||||
///
|
||||
/// Value: rfc1123-date (defined in Section 3.3.1 of [RFC2616])
|
||||
///
|
||||
/// Protected: SHOULD be protected because some clients may rely on the
|
||||
/// value for appropriate caching behavior, or on the value of the
|
||||
/// Last-Modified header to which this property is linked.
|
||||
///
|
||||
/// COPY/MOVE behavior: This property value is dependent on the last
|
||||
/// modified date of the destination resource, not the value of the
|
||||
/// property on the source resource. Note that some server
|
||||
/// implementations use the file system date modified value for the
|
||||
/// DAV:getlastmodified value, and this can be preserved in a MOVE
|
||||
/// even when the HTTP Last-Modified value SHOULD change. Note that
|
||||
/// since [RFC2616] requires clients to use ETags where provided, a
|
||||
/// server implementing ETags can count on clients using a much better
|
||||
/// mechanism than modification dates for offline synchronization or
|
||||
/// cache control. Also note the considerations in Section 8.8.
|
||||
///
|
||||
/// Description: The last-modified date on a resource SHOULD only
|
||||
/// reflect changes in the body (the GET responses) of the resource.
|
||||
/// A change in a property only SHOULD NOT cause the last-modified
|
||||
/// date to change, because clients MAY rely on the last-modified date
|
||||
/// to know when to overwrite the existing body. The DAV:
|
||||
/// getlastmodified property MUST be defined on any DAV-compliant
|
||||
/// resource that returns the Last-Modified header in response to a
|
||||
/// GET.
|
||||
///
|
||||
/// <!ELEMENT getlastmodified (#PCDATA) >
|
||||
GetLastModified(DateTime<FixedOffset>),
|
||||
|
||||
/// 15.8. lockdiscovery Property
|
||||
///
|
||||
/// Name: lockdiscovery
|
||||
///
|
||||
/// Purpose: Describes the active locks on a resource
|
||||
///
|
||||
/// Protected: MUST be protected. Clients change the list of locks
|
||||
/// through LOCK and UNLOCK, not through PROPPATCH.
|
||||
///
|
||||
/// COPY/MOVE behavior: The value of this property depends on the lock
|
||||
/// state of the destination, not on the locks of the source resource.
|
||||
/// Recall that locks are not moved in a MOVE operation.
|
||||
///
|
||||
/// Description: Returns a listing of who has a lock, what type of lock
|
||||
/// he has, the timeout type and the time remaining on the timeout,
|
||||
/// and the associated lock token. Owner information MAY be omitted
|
||||
/// if it is considered sensitive. If there are no locks, but the
|
||||
/// server supports locks, the property will be present but contain
|
||||
/// zero 'activelock' elements. If there are one or more locks, an
|
||||
/// 'activelock' element appears for each lock on the resource. This
|
||||
/// property is NOT lockable with respect to write locks (Section 7).
|
||||
///
|
||||
/// <!ELEMENT lockdiscovery (activelock)* >
|
||||
LockDiscovery(Vec<ActiveLock>),
|
||||
|
||||
|
||||
/// 15.9. resourcetype Property
|
||||
///
|
||||
/// Name: resourcetype
|
||||
///
|
||||
/// Purpose: Specifies the nature of the resource.
|
||||
///
|
||||
/// Protected: SHOULD be protected. Resource type is generally decided
|
||||
/// through the operation creating the resource (MKCOL vs PUT), not by
|
||||
/// PROPPATCH.
|
||||
///
|
||||
/// COPY/MOVE behavior: Generally a COPY/MOVE of a resource results in
|
||||
/// the same type of resource at the destination.
|
||||
///
|
||||
/// Description: MUST be defined on all DAV-compliant resources. Each
|
||||
/// child element identifies a specific type the resource belongs to,
|
||||
/// such as 'collection', which is the only resource type defined by
|
||||
/// this specification (see Section 14.3). If the element contains
|
||||
/// the 'collection' child element plus additional unrecognized
|
||||
/// elements, it should generally be treated as a collection. If the
|
||||
/// element contains no recognized child elements, it should be
|
||||
/// treated as a non-collection resource. The default value is empty.
|
||||
/// This element MUST NOT contain text or mixed content. Any custom
|
||||
/// child element is considered to be an identifier for a resource
|
||||
/// type.
|
||||
///
|
||||
/// Example: (fictional example to show extensibility)
|
||||
///
|
||||
/// <x:resourcetype xmlns:x="DAV:">
|
||||
/// <x:collection/>
|
||||
/// <f:search-results xmlns:f="http://www.example.com/ns"/>
|
||||
/// </x:resourcetype>
|
||||
ResourceType(Vec<ResourceType<E>>),
|
||||
|
||||
/// 15.10. supportedlock Property
|
||||
///
|
||||
/// Name: supportedlock
|
||||
///
|
||||
/// Purpose: To provide a listing of the lock capabilities supported by
|
||||
/// the resource.
|
||||
///
|
||||
/// Protected: MUST be protected. Servers, not clients, determine what
|
||||
/// lock mechanisms are supported.
|
||||
/// COPY/MOVE behavior: This property value is dependent on the kind of
|
||||
/// locks supported at the destination, not on the value of the
|
||||
/// property at the source resource. Servers attempting to COPY to a
|
||||
/// destination should not attempt to set this property at the
|
||||
/// destination.
|
||||
///
|
||||
/// Description: Returns a listing of the combinations of scope and
|
||||
/// access types that may be specified in a lock request on the
|
||||
/// resource. Note that the actual contents are themselves controlled
|
||||
/// by access controls, so a server is not required to provide
|
||||
/// information the client is not authorized to see. This property is
|
||||
/// NOT lockable with respect to write locks (Section 7).
|
||||
///
|
||||
/// <!ELEMENT supportedlock (lockentry)* >
|
||||
SupportedLock(Vec<LockEntry>),
|
||||
|
||||
/// Any extension
|
||||
Extension(E::Property),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum ResourceType<E: Extension> {
|
||||
Collection,
|
||||
Extension(E::ResourceType),
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
//@FIXME required for a full DAV implementation
|
||||
// See section 7.1 of the CalDAV RFC
|
||||
// It seems it's mainly due to the fact that the REPORT method is re-used.
|
|
@ -0,0 +1,321 @@
|
|||
use futures::Future;
|
||||
use quick_xml::events::{Event, BytesStart};
|
||||
use quick_xml::name::ResolveResult;
|
||||
use quick_xml::reader::NsReader;
|
||||
use tokio::io::{AsyncWrite, AsyncBufRead};
|
||||
|
||||
use super::error::ParsingError;
|
||||
|
||||
// Constants
|
||||
pub const DAV_URN: &[u8] = b"DAV:";
|
||||
pub const CAL_URN: &[u8] = b"urn:ietf:params:xml:ns:caldav";
|
||||
pub const CARD_URN: &[u8] = b"urn:ietf:params:xml:ns:carddav";
|
||||
|
||||
// Async traits
|
||||
pub trait IWrite = AsyncWrite + Unpin + Send;
|
||||
pub trait IRead = AsyncBufRead + Unpin;
|
||||
|
||||
// Serialization/Deserialization traits
|
||||
pub trait QWrite {
|
||||
fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> impl Future<Output = Result<(), quick_xml::Error>> + Send;
|
||||
}
|
||||
pub trait QRead<T> {
|
||||
fn qread(xml: &mut Reader<impl IRead>) -> impl Future<Output = Result<T, ParsingError>>;
|
||||
}
|
||||
|
||||
// The representation of an XML node in Rust
|
||||
pub trait Node<T> = QRead<T> + QWrite + std::fmt::Debug + PartialEq + Clone + Sync;
|
||||
|
||||
// ---------------
|
||||
|
||||
/// Transform a Rust object into an XML stream of characters
|
||||
pub struct Writer<T: IWrite> {
|
||||
pub q: quick_xml::writer::Writer<T>,
|
||||
pub ns_to_apply: Vec<(String, String)>,
|
||||
}
|
||||
impl<T: IWrite> Writer<T> {
|
||||
pub fn create_dav_element(&mut self, name: &str) -> BytesStart<'static> {
|
||||
self.create_ns_element("D", name)
|
||||
}
|
||||
pub fn create_cal_element(&mut self, name: &str) -> BytesStart<'static> {
|
||||
self.create_ns_element("C", name)
|
||||
}
|
||||
|
||||
fn create_ns_element(&mut self, ns: &str, name: &str) -> BytesStart<'static> {
|
||||
let mut start = BytesStart::new(format!("{}:{}", ns, name));
|
||||
if !self.ns_to_apply.is_empty() {
|
||||
start.extend_attributes(self.ns_to_apply.iter().map(|(k, n)| (k.as_str(), n.as_str())));
|
||||
self.ns_to_apply.clear()
|
||||
}
|
||||
start
|
||||
}
|
||||
}
|
||||
|
||||
/// Transform an XML stream of characters into a Rust object
|
||||
pub struct Reader<T: IRead> {
|
||||
pub rdr: NsReader<T>,
|
||||
cur: Event<'static>,
|
||||
prev: Event<'static>,
|
||||
parents: Vec<Event<'static>>,
|
||||
buf: Vec<u8>,
|
||||
}
|
||||
impl<T: IRead> Reader<T> {
|
||||
pub async fn new(mut rdr: NsReader<T>) -> Result<Self, ParsingError> {
|
||||
let mut buf: Vec<u8> = vec![];
|
||||
let cur = rdr.read_event_into_async(&mut buf).await?.into_owned();
|
||||
let parents = vec![];
|
||||
let prev = Event::Eof;
|
||||
buf.clear();
|
||||
Ok(Self { cur, prev, parents, rdr, buf })
|
||||
}
|
||||
|
||||
/// read one more tag
|
||||
/// do not expose it publicly
|
||||
async fn next(&mut self) -> Result<Event<'static>, ParsingError> {
|
||||
let evt = self.rdr.read_event_into_async(&mut self.buf).await?.into_owned();
|
||||
self.buf.clear();
|
||||
self.prev = std::mem::replace(&mut self.cur, evt);
|
||||
Ok(self.prev.clone())
|
||||
}
|
||||
|
||||
/// skip a node at current level
|
||||
/// I would like to make this one private but not ready
|
||||
pub async fn skip(&mut self) -> Result<Event<'static>, ParsingError> {
|
||||
//println!("skipping inside node {:?} value {:?}", self.parents.last(), self.cur);
|
||||
match &self.cur {
|
||||
Event::Start(b) => {
|
||||
let _span = self.rdr.read_to_end_into_async(b.to_end().name(), &mut self.buf).await?;
|
||||
self.next().await
|
||||
},
|
||||
Event::End(_) => Err(ParsingError::WrongToken),
|
||||
Event::Eof => Err(ParsingError::Eof),
|
||||
_ => self.next().await,
|
||||
}
|
||||
}
|
||||
|
||||
/// check if this is the desired tag
|
||||
fn is_tag(&self, ns: &[u8], key: &str) -> bool {
|
||||
let qname = match self.peek() {
|
||||
Event::Start(bs) | Event::Empty(bs) => bs.name(),
|
||||
Event::End(be) => be.name(),
|
||||
_ => return false,
|
||||
};
|
||||
|
||||
let (extr_ns, local) = self.rdr.resolve_element(qname);
|
||||
|
||||
if local.into_inner() != key.as_bytes() {
|
||||
return false
|
||||
}
|
||||
|
||||
match extr_ns {
|
||||
ResolveResult::Bound(v) => v.into_inner() == ns,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parent_has_child(&self) -> bool {
|
||||
matches!(self.parents.last(), Some(Event::Start(_)) | None)
|
||||
}
|
||||
|
||||
fn ensure_parent_has_child(&self) -> Result<(), ParsingError> {
|
||||
match self.parent_has_child() {
|
||||
true => Ok(()),
|
||||
false => Err(ParsingError::Recoverable),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn peek(&self) -> &Event<'static> {
|
||||
&self.cur
|
||||
}
|
||||
|
||||
pub fn previous(&self) -> &Event<'static> {
|
||||
&self.prev
|
||||
}
|
||||
|
||||
// NEW API
|
||||
pub async fn tag_string(&mut self) -> Result<String, ParsingError> {
|
||||
self.ensure_parent_has_child()?;
|
||||
|
||||
let mut acc = String::new();
|
||||
loop {
|
||||
match self.peek() {
|
||||
Event::CData(unescaped) => {
|
||||
acc.push_str(std::str::from_utf8(unescaped.as_ref())?);
|
||||
self.next().await?
|
||||
},
|
||||
Event::Text(escaped) => {
|
||||
acc.push_str(escaped.unescape()?.as_ref());
|
||||
self.next().await?
|
||||
}
|
||||
Event::End(_) | Event::Start(_) | Event::Empty(_) => return Ok(acc),
|
||||
_ => self.next().await?,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn maybe_read<N: Node<N>>(&mut self, t: &mut Option<N>, dirty: &mut bool) -> Result<(), ParsingError> {
|
||||
if !self.parent_has_child() {
|
||||
return Ok(())
|
||||
}
|
||||
|
||||
match N::qread(self).await {
|
||||
Ok(v) => {
|
||||
*t = Some(v);
|
||||
*dirty = true;
|
||||
Ok(())
|
||||
},
|
||||
Err(ParsingError::Recoverable) => Ok(()),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn maybe_push<N: Node<N>>(&mut self, t: &mut Vec<N>, dirty: &mut bool) -> Result<(), ParsingError> {
|
||||
if !self.parent_has_child() {
|
||||
return Ok(())
|
||||
}
|
||||
|
||||
match N::qread(self).await {
|
||||
Ok(v) => {
|
||||
t.push(v);
|
||||
*dirty = true;
|
||||
Ok(())
|
||||
},
|
||||
Err(ParsingError::Recoverable) => Ok(()),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn find<N: Node<N>>(&mut self) -> Result<N, ParsingError> {
|
||||
self.ensure_parent_has_child()?;
|
||||
|
||||
loop {
|
||||
// Try parse
|
||||
match N::qread(self).await {
|
||||
Err(ParsingError::Recoverable) => (),
|
||||
otherwise => return otherwise,
|
||||
}
|
||||
|
||||
// If recovered, skip the element
|
||||
self.skip().await?;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn maybe_find<N: Node<N>>(&mut self) -> Result<Option<N>, ParsingError> {
|
||||
self.ensure_parent_has_child()?;
|
||||
|
||||
loop {
|
||||
// Try parse
|
||||
match N::qread(self).await {
|
||||
Err(ParsingError::Recoverable) => (),
|
||||
otherwise => return otherwise.map(Some),
|
||||
}
|
||||
|
||||
match self.peek() {
|
||||
Event::End(_) => return Ok(None),
|
||||
_ => self.skip().await?,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn collect<N: Node<N>>(&mut self) -> Result<Vec<N>, ParsingError> {
|
||||
let mut acc = Vec::new();
|
||||
if !self.parent_has_child() {
|
||||
return Ok(acc)
|
||||
}
|
||||
|
||||
loop {
|
||||
match N::qread(self).await {
|
||||
Err(ParsingError::Recoverable) => match self.peek() {
|
||||
Event::End(_) => return Ok(acc),
|
||||
_ => {
|
||||
self.skip().await?;
|
||||
},
|
||||
},
|
||||
Ok(v) => acc.push(v),
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn open(&mut self, ns: &[u8], key: &str) -> Result<Event<'static>, ParsingError> {
|
||||
//println!("try open tag {:?}, on {:?}", key, self.peek());
|
||||
let evt = match self.peek() {
|
||||
Event::Empty(_) if self.is_tag(ns, key) => {
|
||||
// hack to make `prev_attr` works
|
||||
// here we duplicate the current tag
|
||||
// as in other words, we virtually moved one token
|
||||
// which is useful for prev_attr and any logic based on
|
||||
// self.prev + self.open() on empty nodes
|
||||
self.prev = self.cur.clone();
|
||||
self.cur.clone()
|
||||
},
|
||||
Event::Start(_) if self.is_tag(ns, key) => self.next().await?,
|
||||
_ => return Err(ParsingError::Recoverable),
|
||||
};
|
||||
|
||||
//println!("open tag {:?}", evt);
|
||||
self.parents.push(evt.clone());
|
||||
Ok(evt)
|
||||
}
|
||||
|
||||
pub async fn open_start(&mut self, ns: &[u8], key: &str) -> Result<Event<'static>, ParsingError> {
|
||||
//println!("try open start tag {:?}, on {:?}", key, self.peek());
|
||||
let evt = match self.peek() {
|
||||
Event::Start(_) if self.is_tag(ns, key) => self.next().await?,
|
||||
_ => return Err(ParsingError::Recoverable),
|
||||
};
|
||||
|
||||
//println!("open start tag {:?}", evt);
|
||||
self.parents.push(evt.clone());
|
||||
Ok(evt)
|
||||
}
|
||||
|
||||
pub async fn maybe_open(&mut self, ns: &[u8], key: &str) -> Result<Option<Event<'static>>, ParsingError> {
|
||||
match self.open(ns, key).await {
|
||||
Ok(v) => Ok(Some(v)),
|
||||
Err(ParsingError::Recoverable) => Ok(None),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn maybe_open_start(&mut self, ns: &[u8], key: &str) -> Result<Option<Event<'static>>, ParsingError> {
|
||||
match self.open_start(ns, key).await {
|
||||
Ok(v) => Ok(Some(v)),
|
||||
Err(ParsingError::Recoverable) => Ok(None),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn prev_attr(&self, attr: &str) -> Option<String> {
|
||||
match &self.prev {
|
||||
Event::Start(bs) | Event::Empty(bs) => match bs.try_get_attribute(attr) {
|
||||
Ok(Some(attr)) => attr.decode_and_unescape_value(&self.rdr).ok().map(|v| v.into_owned()),
|
||||
_ => None,
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
// find stop tag
|
||||
pub async fn close(&mut self) -> Result<Event<'static>, ParsingError> {
|
||||
//println!("close tag {:?}", self.parents.last());
|
||||
|
||||
// Handle the empty case
|
||||
if !self.parent_has_child() {
|
||||
self.parents.pop();
|
||||
return self.next().await
|
||||
}
|
||||
|
||||
// Handle the start/end case
|
||||
loop {
|
||||
match self.peek() {
|
||||
Event::End(_) => {
|
||||
self.parents.pop();
|
||||
return self.next().await
|
||||
},
|
||||
_ => self.skip().await?,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
[package]
|
||||
name = "aero-proto"
|
||||
version = "0.3.0"
|
||||
authors = ["Alex Auvolat <alex@adnab.me>", "Quentin Dufour <quentin@dufour.io>"]
|
||||
edition = "2021"
|
||||
license = "EUPL-1.2"
|
||||
description = "Binding between Aerogramme's internal components and well-known protocols"
|
||||
|
||||
[dependencies]
|
||||
aero-sasl.workspace = true
|
||||
aero-dav.workspace = true
|
||||
aero-user.workspace = true
|
||||
aero-collections.workspace = true
|
||||
|
||||
async-trait.workspace = true
|
||||
anyhow.workspace = true
|
||||
hyper.workspace = true
|
||||
base64.workspace = true
|
||||
hyper-util.workspace = true
|
||||
http-body-util.workspace = true
|
||||
futures.workspace = true
|
||||
tokio.workspace = true
|
||||
tokio-util.workspace = true
|
||||
tokio-rustls.workspace = true
|
||||
tokio-stream.workspace = true
|
||||
rustls.workspace = true
|
||||
rustls-pemfile.workspace = true
|
||||
imap-codec.workspace = true
|
||||
imap-flow.workspace = true
|
||||
chrono.workspace = true
|
||||
eml-codec.workspace = true
|
||||
thiserror.workspace = true
|
||||
duplexify.workspace = true
|
||||
smtp-message.workspace = true
|
||||
smtp-server.workspace = true
|
||||
tracing.workspace = true
|
||||
quick-xml.workspace = true
|
|
@ -0,0 +1,108 @@
|
|||
use anyhow::{bail, Result};
|
||||
use hyper::{Request, Response, body::Bytes};
|
||||
use hyper::body::Incoming;
|
||||
use http_body_util::Full;
|
||||
use futures::stream::StreamExt;
|
||||
use futures::stream::TryStreamExt;
|
||||
use http_body_util::BodyStream;
|
||||
use http_body_util::StreamBody;
|
||||
use http_body_util::combinators::UnsyncBoxBody;
|
||||
use hyper::body::Frame;
|
||||
use tokio_util::sync::PollSender;
|
||||
use std::io::{Error, ErrorKind};
|
||||
use futures::sink::SinkExt;
|
||||
use tokio_util::io::{SinkWriter, CopyToBytes};
|
||||
use http_body_util::BodyExt;
|
||||
|
||||
use aero_dav::types as dav;
|
||||
use aero_dav::xml as dxml;
|
||||
use super::controller::HttpResponse;
|
||||
use super::node::PutPolicy;
|
||||
|
||||
pub(crate) fn depth(req: &Request<impl hyper::body::Body>) -> dav::Depth {
|
||||
match req.headers().get("Depth").map(hyper::header::HeaderValue::to_str) {
|
||||
Some(Ok("0")) => dav::Depth::Zero,
|
||||
Some(Ok("1")) => dav::Depth::One,
|
||||
Some(Ok("Infinity")) => dav::Depth::Infinity,
|
||||
_ => dav::Depth::Zero,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn put_policy(req: &Request<impl hyper::body::Body>) -> Result<PutPolicy> {
|
||||
if let Some(maybe_txt_etag) = req.headers().get("If-Match").map(hyper::header::HeaderValue::to_str) {
|
||||
let etag = maybe_txt_etag?;
|
||||
let dquote_count = etag.chars().filter(|c| *c == '"').count();
|
||||
if dquote_count != 2 {
|
||||
bail!("Either If-Match value is invalid or it's not supported (only single etag is supported)");
|
||||
}
|
||||
|
||||
return Ok(PutPolicy::ReplaceEtag(etag.into()))
|
||||
}
|
||||
|
||||
if let Some(maybe_txt_etag) = req.headers().get("If-None-Match").map(hyper::header::HeaderValue::to_str) {
|
||||
let etag = maybe_txt_etag?;
|
||||
if etag == "*" {
|
||||
return Ok(PutPolicy::CreateOnly)
|
||||
}
|
||||
bail!("Either If-None-Match value is invalid or it's not supported (only asterisk is supported)")
|
||||
}
|
||||
|
||||
Ok(PutPolicy::OverwriteAll)
|
||||
}
|
||||
|
||||
pub(crate) fn text_body(txt: &'static str) -> UnsyncBoxBody<Bytes, std::io::Error> {
|
||||
UnsyncBoxBody::new(Full::new(Bytes::from(txt)).map_err(|e| match e {}))
|
||||
}
|
||||
|
||||
pub(crate) fn serialize<T: dxml::QWrite + Send + 'static>(status_ok: hyper::StatusCode, elem: T) -> Result<HttpResponse> {
|
||||
let (tx, rx) = tokio::sync::mpsc::channel::<Bytes>(1);
|
||||
|
||||
// Build the writer
|
||||
tokio::task::spawn(async move {
|
||||
let sink = PollSender::new(tx).sink_map_err(|_| Error::from(ErrorKind::BrokenPipe));
|
||||
let mut writer = SinkWriter::new(CopyToBytes::new(sink));
|
||||
let q = quick_xml::writer::Writer::new_with_indent(&mut writer, b' ', 4);
|
||||
let ns_to_apply = vec![ ("xmlns:D".into(), "DAV:".into()), ("xmlns:C".into(), "urn:ietf:params:xml:ns:caldav".into()) ];
|
||||
let mut qwriter = dxml::Writer { q, ns_to_apply };
|
||||
let decl = quick_xml::events::BytesDecl::from_start(quick_xml::events::BytesStart::from_content("xml version=\"1.0\" encoding=\"utf-8\"", 0));
|
||||
match qwriter.q.write_event_async(quick_xml::events::Event::Decl(decl)).await {
|
||||
Ok(_) => (),
|
||||
Err(e) => tracing::error!(err=?e, "unable to write XML declaration <?xml ... >"),
|
||||
}
|
||||
match elem.qwrite(&mut qwriter).await {
|
||||
Ok(_) => tracing::debug!("fully serialized object"),
|
||||
Err(e) => tracing::error!(err=?e, "failed to serialize object"),
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Build the reader
|
||||
let recv = tokio_stream::wrappers::ReceiverStream::new(rx);
|
||||
let stream = StreamBody::new(recv.map(|v| Ok(Frame::data(v))));
|
||||
let boxed_body = UnsyncBoxBody::new(stream);
|
||||
|
||||
let response = Response::builder()
|
||||
.status(status_ok)
|
||||
.header("content-type", "application/xml; charset=\"utf-8\"")
|
||||
.body(boxed_body)?;
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
|
||||
/// Deserialize a request body to an XML request
|
||||
pub(crate) async fn deserialize<T: dxml::Node<T>>(req: Request<Incoming>) -> Result<T> {
|
||||
let stream_of_frames = BodyStream::new(req.into_body());
|
||||
let stream_of_bytes = stream_of_frames
|
||||
.map_ok(|frame| frame.into_data())
|
||||
.map(|obj| match obj {
|
||||
Ok(Ok(v)) => Ok(v),
|
||||
Ok(Err(_)) => Err(std::io::Error::new(std::io::ErrorKind::Other, "conversion error")),
|
||||
Err(err) => Err(std::io::Error::new(std::io::ErrorKind::Other, err)),
|
||||
});
|
||||
let async_read = tokio_util::io::StreamReader::new(stream_of_bytes);
|
||||
let async_read = std::pin::pin!(async_read);
|
||||
let mut rdr = dxml::Reader::new(quick_xml::reader::NsReader::from_reader(async_read)).await?;
|
||||
let parsed = rdr.find::<T>().await?;
|
||||
Ok(parsed)
|
||||
}
|
|
@ -0,0 +1,278 @@
|
|||
use anyhow::Result;
|
||||
use http_body_util::combinators::{UnsyncBoxBody, BoxBody};
|
||||
use hyper::body::Incoming;
|
||||
use hyper::{Request, Response, body::Bytes};
|
||||
use http_body_util::BodyStream;
|
||||
use http_body_util::StreamBody;
|
||||
use hyper::body::Frame;
|
||||
use futures::stream::{StreamExt, TryStreamExt};
|
||||
|
||||
use aero_collections::user::User;
|
||||
use aero_dav::types as dav;
|
||||
use aero_dav::realization::All;
|
||||
use aero_dav::caltypes as cal;
|
||||
|
||||
use crate::dav::codec::{serialize, deserialize, depth, text_body};
|
||||
use crate::dav::node::{DavNode, PutPolicy};
|
||||
use crate::dav::resource::RootNode;
|
||||
use crate::dav::codec;
|
||||
|
||||
pub(super) type ArcUser = std::sync::Arc<User>;
|
||||
pub(super) type HttpResponse = Response<UnsyncBoxBody<Bytes, std::io::Error>>;
|
||||
|
||||
const ALLPROP: [dav::PropertyRequest<All>; 10] = [
|
||||
dav::PropertyRequest::CreationDate,
|
||||
dav::PropertyRequest::DisplayName,
|
||||
dav::PropertyRequest::GetContentLanguage,
|
||||
dav::PropertyRequest::GetContentLength,
|
||||
dav::PropertyRequest::GetContentType,
|
||||
dav::PropertyRequest::GetEtag,
|
||||
dav::PropertyRequest::GetLastModified,
|
||||
dav::PropertyRequest::LockDiscovery,
|
||||
dav::PropertyRequest::ResourceType,
|
||||
dav::PropertyRequest::SupportedLock,
|
||||
];
|
||||
|
||||
pub(crate) struct Controller {
|
||||
node: Box<dyn DavNode>,
|
||||
user: std::sync::Arc<User>,
|
||||
req: Request<Incoming>,
|
||||
}
|
||||
impl Controller {
|
||||
pub(crate) async fn route(user: std::sync::Arc<User>, req: Request<Incoming>) -> Result<HttpResponse> {
|
||||
let path = req.uri().path().to_string();
|
||||
let path_segments: Vec<_> = path.split("/").filter(|s| *s != "").collect();
|
||||
let method = req.method().as_str().to_uppercase();
|
||||
|
||||
let can_create = matches!(method.as_str(), "PUT" | "MKCOL" | "MKCALENDAR");
|
||||
let node = match (RootNode {}).fetch(&user, &path_segments, can_create).await{
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
tracing::warn!(err=?e, "dav node fetch failed");
|
||||
return Ok(Response::builder()
|
||||
.status(404)
|
||||
.body(codec::text_body("Resource not found"))?)
|
||||
}
|
||||
};
|
||||
|
||||
let ctrl = Self { node, user, req };
|
||||
|
||||
match method.as_str() {
|
||||
"OPTIONS" => Ok(Response::builder()
|
||||
.status(200)
|
||||
.header("DAV", "1")
|
||||
.header("Allow", "HEAD,GET,PUT,OPTIONS,DELETE,PROPFIND,PROPPATCH,MKCOL,COPY,MOVE,LOCK,UNLOCK,MKCALENDAR,REPORT")
|
||||
.body(codec::text_body(""))?),
|
||||
"HEAD" => {
|
||||
tracing::warn!("HEAD not correctly implemented");
|
||||
Ok(Response::builder()
|
||||
.status(404)
|
||||
.body(codec::text_body(""))?)
|
||||
},
|
||||
"GET" => ctrl.get().await,
|
||||
"PUT" => ctrl.put().await,
|
||||
"DELETE" => ctrl.delete().await,
|
||||
"PROPFIND" => ctrl.propfind().await,
|
||||
"REPORT" => ctrl.report().await,
|
||||
_ => Ok(Response::builder()
|
||||
.status(501)
|
||||
.body(codec::text_body("HTTP Method not implemented"))?),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// --- Per-method functions ---
|
||||
|
||||
/// REPORT has been first described in the "Versioning Extension" of WebDAV
|
||||
/// It allows more complex queries compared to PROPFIND
|
||||
///
|
||||
/// Note: current implementation is not generic at all, it is heavily tied to CalDAV.
|
||||
/// A rewrite would be required to make it more generic (with the extension system that has
|
||||
/// been introduced in aero-dav)
|
||||
async fn report(self) -> Result<HttpResponse> {
|
||||
let status = hyper::StatusCode::from_u16(207)?;
|
||||
|
||||
let report = match deserialize::<cal::Report<All>>(self.req).await {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
tracing::error!(err=?e, "unable to decode REPORT body");
|
||||
return Ok(Response::builder()
|
||||
.status(400)
|
||||
.body(text_body("Bad request"))?)
|
||||
}
|
||||
};
|
||||
|
||||
// Multiget is really like a propfind where Depth: 0|1|Infinity is replaced by an arbitrary
|
||||
// list of URLs
|
||||
// @FIXME
|
||||
let multiget = match report {
|
||||
cal::Report::Multiget(m) => m,
|
||||
cal::Report::Query(q) => todo!(),
|
||||
cal::Report::FreeBusy(_) => return Ok(Response::builder()
|
||||
.status(501)
|
||||
.body(text_body("Not implemented"))?),
|
||||
};
|
||||
|
||||
// Getting the list of nodes
|
||||
let (mut ok_node, mut not_found) = (Vec::new(), Vec::new());
|
||||
for h in multiget.href.into_iter() {
|
||||
let maybe_collected_node = match Path::new(h.0.as_str()) {
|
||||
Ok(Path::Abs(p)) => RootNode{}.fetch(&self.user, p.as_slice(), false).await.or(Err(h)),
|
||||
Ok(Path::Rel(p)) => self.node.fetch(&self.user, p.as_slice(), false).await.or(Err(h)),
|
||||
Err(_) => Err(h),
|
||||
};
|
||||
|
||||
match maybe_collected_node {
|
||||
Ok(v) => ok_node.push(v),
|
||||
Err(h) => not_found.push(h),
|
||||
};
|
||||
}
|
||||
|
||||
// Getting props
|
||||
let props = match multiget.selector {
|
||||
None | Some(cal::CalendarSelector::AllProp) => Some(dav::PropName(ALLPROP.to_vec())),
|
||||
Some(cal::CalendarSelector::PropName) => None,
|
||||
Some(cal::CalendarSelector::Prop(inner)) => Some(inner),
|
||||
};
|
||||
|
||||
serialize(status, Self::multistatus(&self.user, ok_node, not_found, props).await)
|
||||
}
|
||||
|
||||
/// PROPFIND is the standard way to fetch WebDAV properties
|
||||
async fn propfind(self) -> Result<HttpResponse> {
|
||||
let depth = depth(&self.req);
|
||||
if matches!(depth, dav::Depth::Infinity) {
|
||||
return Ok(Response::builder()
|
||||
.status(501)
|
||||
.body(text_body("Depth: Infinity not implemented"))?)
|
||||
}
|
||||
|
||||
let status = hyper::StatusCode::from_u16(207)?;
|
||||
|
||||
// A client may choose not to submit a request body. An empty PROPFIND
|
||||
// request body MUST be treated as if it were an 'allprop' request.
|
||||
// @FIXME here we handle any invalid data as an allprop, an empty request is thus correctly
|
||||
// handled, but corrupted requests are also silently handled as allprop.
|
||||
let propfind = deserialize::<dav::PropFind<All>>(self.req).await.unwrap_or_else(|_| dav::PropFind::<All>::AllProp(None));
|
||||
tracing::debug!(recv=?propfind, "inferred propfind request");
|
||||
|
||||
// Collect nodes as PROPFIND is not limited to the targeted node
|
||||
let mut nodes = vec![];
|
||||
if matches!(depth, dav::Depth::One | dav::Depth::Infinity) {
|
||||
nodes.extend(self.node.children(&self.user).await);
|
||||
}
|
||||
nodes.push(self.node);
|
||||
|
||||
// Expand properties request
|
||||
let propname = match propfind {
|
||||
dav::PropFind::PropName => None,
|
||||
dav::PropFind::AllProp(None) => Some(dav::PropName(ALLPROP.to_vec())),
|
||||
dav::PropFind::AllProp(Some(dav::Include(mut include))) => {
|
||||
include.extend_from_slice(&ALLPROP);
|
||||
Some(dav::PropName(include))
|
||||
},
|
||||
dav::PropFind::Prop(inner) => Some(inner),
|
||||
};
|
||||
|
||||
// Not Found is currently impossible considering the way we designed this function
|
||||
let not_found = vec![];
|
||||
serialize(status, Self::multistatus(&self.user, nodes, not_found, propname).await)
|
||||
}
|
||||
|
||||
async fn put(self) -> Result<HttpResponse> {
|
||||
let put_policy = codec::put_policy(&self.req)?;
|
||||
|
||||
let stream_of_frames = BodyStream::new(self.req.into_body());
|
||||
let stream_of_bytes = stream_of_frames
|
||||
.map_ok(|frame| frame.into_data())
|
||||
.map(|obj| match obj {
|
||||
Ok(Ok(v)) => Ok(v),
|
||||
Ok(Err(_)) => Err(std::io::Error::new(std::io::ErrorKind::Other, "conversion error")),
|
||||
Err(err) => Err(std::io::Error::new(std::io::ErrorKind::Other, err)),
|
||||
}).boxed();
|
||||
|
||||
let etag = self.node.put(put_policy, stream_of_bytes).await?;
|
||||
|
||||
let response = Response::builder()
|
||||
.status(201)
|
||||
.header("ETag", etag)
|
||||
//.header("content-type", "application/xml; charset=\"utf-8\"")
|
||||
.body(text_body(""))?;
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
async fn get(self) -> Result<HttpResponse> {
|
||||
let stream_body = StreamBody::new(self.node.content().map_ok(|v| Frame::data(v)));
|
||||
let boxed_body = UnsyncBoxBody::new(stream_body);
|
||||
|
||||
let mut builder = Response::builder().status(200);
|
||||
builder = builder.header("content-type", self.node.content_type());
|
||||
if let Some(etag) = self.node.etag().await {
|
||||
builder = builder.header("etag", etag);
|
||||
}
|
||||
let response = builder.body(boxed_body)?;
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
async fn delete(self) -> Result<HttpResponse> {
|
||||
self.node.delete().await?;
|
||||
let response = Response::builder()
|
||||
.status(204)
|
||||
//.header("content-type", "application/xml; charset=\"utf-8\"")
|
||||
.body(text_body(""))?;
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
// --- Common utility functions ---
|
||||
/// Build a multistatus response from a list of DavNodes
|
||||
async fn multistatus(user: &ArcUser, nodes: Vec<Box<dyn DavNode>>, not_found: Vec<dav::Href>, props: Option<dav::PropName<All>>) -> dav::Multistatus<All> {
|
||||
// Collect properties on existing objects
|
||||
let mut responses: Vec<dav::Response<All>> = match props {
|
||||
Some(props) => futures::stream::iter(nodes).then(|n| n.response_props(user, props.clone())).collect().await,
|
||||
None => nodes.into_iter().map(|n| n.response_propname(user)).collect(),
|
||||
};
|
||||
|
||||
// Register not found objects only if relevant
|
||||
if !not_found.is_empty() {
|
||||
responses.push(dav::Response {
|
||||
status_or_propstat: dav::StatusOrPropstat::Status(not_found, dav::Status(hyper::StatusCode::NOT_FOUND)),
|
||||
error: None,
|
||||
location: None,
|
||||
responsedescription: None,
|
||||
});
|
||||
}
|
||||
|
||||
// Build response
|
||||
dav::Multistatus::<All> {
|
||||
responses,
|
||||
responsedescription: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Path is a voluntarily feature limited
|
||||
/// compared to the expressiveness of a UNIX path
|
||||
/// For example getting parent with ../ is not supported, scheme is not supported, etc.
|
||||
/// More complex support could be added later if needed by clients
|
||||
enum Path<'a> {
|
||||
Abs(Vec<&'a str>),
|
||||
Rel(Vec<&'a str>),
|
||||
}
|
||||
impl<'a> Path<'a> {
|
||||
fn new(path: &'a str) -> Result<Self> {
|
||||
// This check is naive, it does not aim at detecting all fully qualified
|
||||
// URL or protect from any attack, its only goal is to help debugging.
|
||||
if path.starts_with("http://") || path.starts_with("https://") {
|
||||
anyhow::bail!("Full URL are not supported")
|
||||
}
|
||||
|
||||
let path_segments: Vec<_> = path.split("/").filter(|s| *s != "" && *s != ".").collect();
|
||||
if path.starts_with("/") {
|
||||
return Ok(Path::Abs(path_segments))
|
||||
}
|
||||
Ok(Path::Rel(path_segments))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
use anyhow::{anyhow, Result};
|
||||
use base64::Engine;
|
||||
use hyper::{Request, Response};
|
||||
use hyper::body::Incoming;
|
||||
|
||||
use aero_user::login::ArcLoginProvider;
|
||||
use aero_collections::user::User;
|
||||
|
||||
use super::codec::text_body;
|
||||
use super::controller::HttpResponse;
|
||||
|
||||
type ArcUser = std::sync::Arc<User>;
|
||||
|
||||
pub(super) async fn auth<'a>(
|
||||
login: ArcLoginProvider,
|
||||
req: Request<Incoming>,
|
||||
next: impl Fn(ArcUser, Request<Incoming>) -> futures::future::BoxFuture<'a, Result<HttpResponse>>,
|
||||
) -> Result<HttpResponse> {
|
||||
let auth_val = match req.headers().get(hyper::header::AUTHORIZATION) {
|
||||
Some(hv) => hv.to_str()?,
|
||||
None => {
|
||||
tracing::info!("Missing authorization field");
|
||||
return Ok(Response::builder()
|
||||
.status(401)
|
||||
.header("WWW-Authenticate", "Basic realm=\"Aerogramme\"")
|
||||
.body(text_body("Missing Authorization field"))?)
|
||||
},
|
||||
};
|
||||
|
||||
let b64_creds_maybe_padded = match auth_val.split_once(" ") {
|
||||
Some(("Basic", b64)) => b64,
|
||||
_ => {
|
||||
tracing::info!("Unsupported authorization field");
|
||||
return Ok(Response::builder()
|
||||
.status(400)
|
||||
.body(text_body("Unsupported Authorization field"))?)
|
||||
},
|
||||
};
|
||||
|
||||
// base64urlencoded may have trailing equals, base64urlsafe has not
|
||||
// theoretically authorization is padded but "be liberal in what you accept"
|
||||
let b64_creds_clean = b64_creds_maybe_padded.trim_end_matches('=');
|
||||
|
||||
// Decode base64
|
||||
let creds = base64::engine::general_purpose::STANDARD_NO_PAD.decode(b64_creds_clean)?;
|
||||
let str_creds = std::str::from_utf8(&creds)?;
|
||||
|
||||
// Split username and password
|
||||
let (username, password) = str_creds
|
||||
.split_once(':')
|
||||
.ok_or(anyhow!("Missing colon in Authorization, can't split decoded value into a username/password pair"))?;
|
||||
|
||||
// Call login provider
|
||||
let creds = match login.login(username, password).await {
|
||||
Ok(c) => c,
|
||||
Err(_) => {
|
||||
tracing::info!(user=username, "Wrong credentials");
|
||||
return Ok(Response::builder()
|
||||
.status(401)
|
||||
.header("WWW-Authenticate", "Basic realm=\"Aerogramme\"")
|
||||
.body(text_body("Wrong credentials"))?)
|
||||
},
|
||||
};
|
||||
|
||||
// Build a user
|
||||
let user = User::new(username.into(), creds).await?;
|
||||
|
||||
// Call router with user
|
||||
next(user, req).await
|
||||
}
|
|
@ -0,0 +1,183 @@
|
|||
mod middleware;
|
||||
mod controller;
|
||||
mod codec;
|
||||
mod node;
|
||||
mod resource;
|
||||
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use hyper::service::service_fn;
|
||||
use hyper::{Request, Response};
|
||||
use hyper::server::conn::http1 as http;
|
||||
use hyper::rt::{Read, Write};
|
||||
use hyper_util::rt::TokioIo;
|
||||
use futures::stream::{FuturesUnordered, StreamExt};
|
||||
use tokio::net::TcpListener;
|
||||
use tokio::sync::watch;
|
||||
use tokio_rustls::TlsAcceptor;
|
||||
use tokio::net::TcpStream;
|
||||
use futures::future::FutureExt;
|
||||
use tokio::io::{AsyncRead, AsyncWrite};
|
||||
use rustls_pemfile::{certs, private_key};
|
||||
|
||||
use aero_user::config::{DavConfig, DavUnsecureConfig};
|
||||
use aero_user::login::ArcLoginProvider;
|
||||
|
||||
use crate::dav::controller::Controller;
|
||||
|
||||
pub struct Server {
|
||||
bind_addr: SocketAddr,
|
||||
login_provider: ArcLoginProvider,
|
||||
tls: Option<TlsAcceptor>,
|
||||
}
|
||||
|
||||
pub fn new_unsecure(config: DavUnsecureConfig, login: ArcLoginProvider) -> Server {
|
||||
Server {
|
||||
bind_addr: config.bind_addr,
|
||||
login_provider: login,
|
||||
tls: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new(config: DavConfig, login: ArcLoginProvider) -> Result<Server> {
|
||||
let loaded_certs = certs(&mut std::io::BufReader::new(std::fs::File::open(
|
||||
config.certs,
|
||||
)?))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let loaded_key = private_key(&mut std::io::BufReader::new(std::fs::File::open(
|
||||
config.key,
|
||||
)?))?
|
||||
.unwrap();
|
||||
|
||||
let tls_config = rustls::ServerConfig::builder()
|
||||
.with_no_client_auth()
|
||||
.with_single_cert(loaded_certs, loaded_key)?;
|
||||
let acceptor = TlsAcceptor::from(Arc::new(tls_config));
|
||||
|
||||
Ok(Server {
|
||||
bind_addr: config.bind_addr,
|
||||
login_provider: login,
|
||||
tls: Some(acceptor),
|
||||
})
|
||||
}
|
||||
|
||||
trait Stream: Read + Write + Send + Unpin {}
|
||||
impl<T: Unpin + AsyncRead + AsyncWrite + Send> Stream for TokioIo<T> {}
|
||||
|
||||
impl Server {
|
||||
pub async fn run(self: Self, mut must_exit: watch::Receiver<bool>) -> Result<()> {
|
||||
let tcp = TcpListener::bind(self.bind_addr).await?;
|
||||
tracing::info!("DAV server listening on {:#}", self.bind_addr);
|
||||
|
||||
let mut connections = FuturesUnordered::new();
|
||||
while !*must_exit.borrow() {
|
||||
let wait_conn_finished = async {
|
||||
if connections.is_empty() {
|
||||
futures::future::pending().await
|
||||
} else {
|
||||
connections.next().await
|
||||
}
|
||||
};
|
||||
let (socket, remote_addr) = tokio::select! {
|
||||
a = tcp.accept() => a?,
|
||||
_ = wait_conn_finished => continue,
|
||||
_ = must_exit.changed() => continue,
|
||||
};
|
||||
tracing::info!("Accepted connection from {}", remote_addr);
|
||||
let stream = match self.build_stream(socket).await {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
tracing::error!(err=?e, "TLS acceptor failed");
|
||||
continue
|
||||
}
|
||||
};
|
||||
|
||||
let login = self.login_provider.clone();
|
||||
let conn = tokio::spawn(async move {
|
||||
//@FIXME should create a generic "public web" server on which "routers" could be
|
||||
//abitrarily bound
|
||||
//@FIXME replace with a handler supporting http2 and TLS
|
||||
|
||||
match http::Builder::new().serve_connection(stream, service_fn(|req: Request<hyper::body::Incoming>| {
|
||||
let login = login.clone();
|
||||
tracing::info!("{:?} {:?}", req.method(), req.uri());
|
||||
async {
|
||||
match middleware::auth(login, req, |user, request| async { Controller::route(user, request).await }.boxed()).await {
|
||||
Ok(v) => Ok(v),
|
||||
Err(e) => {
|
||||
tracing::error!(err=?e, "internal error");
|
||||
Response::builder()
|
||||
.status(500)
|
||||
.body(codec::text_body("Internal error"))
|
||||
},
|
||||
}
|
||||
}
|
||||
})).await {
|
||||
Err(e) => tracing::warn!(err=?e, "connection failed"),
|
||||
Ok(()) => tracing::trace!("connection terminated with success"),
|
||||
}
|
||||
});
|
||||
connections.push(conn);
|
||||
}
|
||||
drop(tcp);
|
||||
|
||||
tracing::info!("Server shutting down, draining remaining connections...");
|
||||
while connections.next().await.is_some() {}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn build_stream(&self, socket: TcpStream) -> Result<Box<dyn Stream>> {
|
||||
match self.tls.clone() {
|
||||
Some(acceptor) => {
|
||||
let stream = acceptor.accept(socket).await?;
|
||||
Ok(Box::new(TokioIo::new(stream)))
|
||||
}
|
||||
None => Ok(Box::new(TokioIo::new(socket))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// <D:propfind xmlns:D='DAV:' xmlns:A='http://apple.com/ns/ical/'>
|
||||
// <D:prop>
|
||||
// <D:getcontenttype/>
|
||||
// <D:resourcetype/>
|
||||
// <D:displayname/>
|
||||
// <A:calendar-color/>
|
||||
// </D:prop>
|
||||
// </D:propfind>
|
||||
|
||||
|
||||
// <D:propfind xmlns:D='DAV:' xmlns:A='http://apple.com/ns/ical/' xmlns:C='urn:ietf:params:xml:ns:caldav'>
|
||||
// <D:prop>
|
||||
// <D:resourcetype/>
|
||||
// <D:owner/>
|
||||
// <D:displayname/>
|
||||
// <D:current-user-principal/>
|
||||
// <D:current-user-privilege-set/>
|
||||
// <A:calendar-color/>
|
||||
// <C:calendar-home-set/>
|
||||
// </D:prop>
|
||||
// </D:propfind>
|
||||
|
||||
// <D:propfind xmlns:D='DAV:' xmlns:C='urn:ietf:params:xml:ns:caldav' xmlns:CS='http://calendarserver.org/ns/'>
|
||||
// <D:prop>
|
||||
// <D:resourcetype/>
|
||||
// <D:owner/>
|
||||
// <D:current-user-principal/>
|
||||
// <D:current-user-privilege-set/>
|
||||
// <D:supported-report-set/>
|
||||
// <C:supported-calendar-component-set/>
|
||||
// <CS:getctag/>
|
||||
// </D:prop>
|
||||
// </D:propfind>
|
||||
|
||||
// <C:calendar-multiget xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
|
||||
// <D:prop>
|
||||
// <D:getetag/>
|
||||
// <C:calendar-data/>
|
||||
// </D:prop>
|
||||
// <D:href>/alice/calendar/personal/something.ics</D:href>
|
||||
// </C:calendar-multiget>
|
|
@ -0,0 +1,119 @@
|
|||
use anyhow::Result;
|
||||
use futures::stream::{BoxStream, StreamExt};
|
||||
use futures::future::{BoxFuture, FutureExt};
|
||||
use hyper::body::Bytes;
|
||||
|
||||
use aero_dav::types as dav;
|
||||
use aero_dav::realization::All;
|
||||
use aero_collections::davdag::Etag;
|
||||
|
||||
use super::controller::ArcUser;
|
||||
|
||||
pub(crate) type Content<'a> = BoxStream<'a, std::result::Result<Bytes, std::io::Error>>;
|
||||
pub(crate) type PropertyStream<'a> = BoxStream<'a, std::result::Result<dav::Property<All>, dav::PropertyRequest<All>>>;
|
||||
|
||||
pub(crate) enum PutPolicy {
|
||||
OverwriteAll,
|
||||
CreateOnly,
|
||||
ReplaceEtag(String),
|
||||
}
|
||||
|
||||
/// A DAV node should implement the following methods
|
||||
/// @FIXME not satisfied by BoxFutures but I have no better idea currently
|
||||
pub(crate) trait DavNode: Send {
|
||||
// recurence, filesystem hierarchy
|
||||
/// This node direct children
|
||||
fn children<'a>(&self, user: &'a ArcUser) -> BoxFuture<'a, Vec<Box<dyn DavNode>>>;
|
||||
/// Recursively fetch a child (progress inside the filesystem hierarchy)
|
||||
fn fetch<'a>(&self, user: &'a ArcUser, path: &'a [&str], create: bool) -> BoxFuture<'a, Result<Box<dyn DavNode>>>;
|
||||
|
||||
// node properties
|
||||
/// Get the path
|
||||
fn path(&self, user: &ArcUser) -> String;
|
||||
/// Get the supported WebDAV properties
|
||||
fn supported_properties(&self, user: &ArcUser) -> dav::PropName<All>;
|
||||
/// Get the values for the given properties
|
||||
fn properties(&self, user: &ArcUser, prop: dav::PropName<All>) -> PropertyStream<'static>;
|
||||
//fn properties(&self, user: &ArcUser, prop: dav::PropName<All>) -> Vec<dav::AnyProperty<All>>;
|
||||
/// Put an element (create or update)
|
||||
fn put<'a>(&'a self, policy: PutPolicy, stream: Content<'a>) -> BoxFuture<'a, std::result::Result<Etag, std::io::Error>>;
|
||||
/// Content type of the element
|
||||
fn content_type(&self) -> &str;
|
||||
/// Get ETag
|
||||
fn etag(&self) -> BoxFuture<Option<Etag>>;
|
||||
/// Get content
|
||||
fn content(&self) -> Content<'static>;
|
||||
/// Delete
|
||||
fn delete(&self) -> BoxFuture<std::result::Result<(), std::io::Error>>;
|
||||
|
||||
//@FIXME maybe add etag, maybe add a way to set content
|
||||
|
||||
/// Utility function to get a propname response from a node
|
||||
fn response_propname(&self, user: &ArcUser) -> dav::Response<All> {
|
||||
dav::Response {
|
||||
status_or_propstat: dav::StatusOrPropstat::PropStat(
|
||||
dav::Href(self.path(user)),
|
||||
vec![
|
||||
dav::PropStat {
|
||||
status: dav::Status(hyper::StatusCode::OK),
|
||||
prop: dav::AnyProp(self.supported_properties(user).0.into_iter().map(dav::AnyProperty::Request).collect()),
|
||||
error: None,
|
||||
responsedescription: None,
|
||||
}
|
||||
],
|
||||
),
|
||||
error: None,
|
||||
location: None,
|
||||
responsedescription: None
|
||||
}
|
||||
}
|
||||
|
||||
/// Utility function to get a prop response from a node & a list of propname
|
||||
fn response_props(&self, user: &ArcUser, props: dav::PropName<All>) -> BoxFuture<'static, dav::Response<All>> {
|
||||
//@FIXME we should make the DAV parsed object a stream...
|
||||
let mut result_stream = self.properties(user, props);
|
||||
let path = self.path(user);
|
||||
|
||||
async move {
|
||||
let mut prop_desc = vec![];
|
||||
let (mut found, mut not_found) = (vec![], vec![]);
|
||||
while let Some(maybe_prop) = result_stream.next().await {
|
||||
match maybe_prop {
|
||||
Ok(v) => found.push(dav::AnyProperty::Value(v)),
|
||||
Err(v) => not_found.push(dav::AnyProperty::Request(v)),
|
||||
}
|
||||
}
|
||||
|
||||
// If at least one property has been found on this object, adding a HTTP 200 propstat to
|
||||
// the response
|
||||
if !found.is_empty() {
|
||||
prop_desc.push(dav::PropStat {
|
||||
status: dav::Status(hyper::StatusCode::OK),
|
||||
prop: dav::AnyProp(found),
|
||||
error: None,
|
||||
responsedescription: None,
|
||||
});
|
||||
}
|
||||
|
||||
// If at least one property can't be found on this object, adding a HTTP 404 propstat to
|
||||
// the response
|
||||
if !not_found.is_empty() {
|
||||
prop_desc.push(dav::PropStat {
|
||||
status: dav::Status(hyper::StatusCode::NOT_FOUND),
|
||||
prop: dav::AnyProp(not_found),
|
||||
error: None,
|
||||
responsedescription: None,
|
||||
})
|
||||
}
|
||||
|
||||
// Build the finale response
|
||||
dav::Response {
|
||||
status_or_propstat: dav::StatusOrPropstat::PropStat(dav::Href(path), prop_desc),
|
||||
error: None,
|
||||
location: None,
|
||||
responsedescription: None
|
||||
}
|
||||
}.boxed()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,568 @@
|
|||
use std::sync::Arc;
|
||||
type ArcUser = std::sync::Arc<User>;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use futures::stream::{TryStreamExt, StreamExt};
|
||||
use futures::io::AsyncReadExt;
|
||||
use futures::{future::BoxFuture, future::FutureExt};
|
||||
|
||||
use aero_collections::{user::User, calendar::Calendar, davdag::{BlobId, Etag}};
|
||||
use aero_dav::types as dav;
|
||||
use aero_dav::caltypes as cal;
|
||||
use aero_dav::acltypes as acl;
|
||||
use aero_dav::realization::{All, self as all};
|
||||
|
||||
use crate::dav::node::{DavNode, PutPolicy, Content};
|
||||
use super::node::PropertyStream;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct RootNode {}
|
||||
impl DavNode for RootNode {
|
||||
fn fetch<'a>(&self, user: &'a ArcUser, path: &'a [&str], create: bool) -> BoxFuture<'a, Result<Box<dyn DavNode>>> {
|
||||
if path.len() == 0 {
|
||||
let this = self.clone();
|
||||
return async { Ok(Box::new(this) as Box<dyn DavNode>) }.boxed();
|
||||
}
|
||||
|
||||
if path[0] == user.username {
|
||||
let child = Box::new(HomeNode {});
|
||||
return child.fetch(user, &path[1..], create);
|
||||
}
|
||||
|
||||
//@NOTE: We can't create a node at this level
|
||||
async { Err(anyhow!("Not found")) }.boxed()
|
||||
}
|
||||
|
||||
fn children<'a>(&self, user: &'a ArcUser) -> BoxFuture<'a, Vec<Box<dyn DavNode>>> {
|
||||
async { vec![Box::new(HomeNode { }) as Box<dyn DavNode>] }.boxed()
|
||||
}
|
||||
|
||||
fn path(&self, user: &ArcUser) -> String {
|
||||
"/".into()
|
||||
}
|
||||
|
||||
fn supported_properties(&self, user: &ArcUser) -> dav::PropName<All> {
|
||||
dav::PropName(vec![
|
||||
dav::PropertyRequest::DisplayName,
|
||||
dav::PropertyRequest::ResourceType,
|
||||
dav::PropertyRequest::GetContentType,
|
||||
dav::PropertyRequest::Extension(all::PropertyRequest::Acl(acl::PropertyRequest::CurrentUserPrincipal)),
|
||||
])
|
||||
}
|
||||
|
||||
fn properties(&self, user: &ArcUser, prop: dav::PropName<All>) -> PropertyStream<'static> {
|
||||
let user = user.clone();
|
||||
futures::stream::iter(prop.0).map(move |n| {
|
||||
let prop = match n {
|
||||
dav::PropertyRequest::DisplayName => dav::Property::DisplayName("DAV Root".to_string()),
|
||||
dav::PropertyRequest::ResourceType => dav::Property::ResourceType(vec![
|
||||
dav::ResourceType::Collection,
|
||||
]),
|
||||
dav::PropertyRequest::GetContentType => dav::Property::GetContentType("httpd/unix-directory".into()),
|
||||
dav::PropertyRequest::Extension(all::PropertyRequest::Acl(acl::PropertyRequest::CurrentUserPrincipal)) =>
|
||||
dav::Property::Extension(all::Property::Acl(acl::Property::CurrentUserPrincipal(acl::User::Authenticated(dav::Href(HomeNode{}.path(&user)))))),
|
||||
v => return Err(v),
|
||||
};
|
||||
Ok(prop)
|
||||
}).boxed()
|
||||
}
|
||||
|
||||
fn put<'a>(&'a self, _policy: PutPolicy, stream: Content<'a>) -> BoxFuture<'a, std::result::Result<Etag, std::io::Error>> {
|
||||
futures::future::err(std::io::Error::from(std::io::ErrorKind::Unsupported)).boxed()
|
||||
}
|
||||
|
||||
fn content(&self) -> Content<'static> {
|
||||
futures::stream::once(futures::future::err(std::io::Error::from(std::io::ErrorKind::Unsupported))).boxed()
|
||||
}
|
||||
|
||||
fn content_type(&self) -> &str {
|
||||
"text/plain"
|
||||
}
|
||||
|
||||
fn etag(&self) -> BoxFuture<Option<Etag>> {
|
||||
async { None }.boxed()
|
||||
}
|
||||
|
||||
fn delete(&self) -> BoxFuture<std::result::Result<(), std::io::Error>> {
|
||||
async { Err(std::io::Error::from(std::io::ErrorKind::PermissionDenied)) }.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct HomeNode {}
|
||||
impl DavNode for HomeNode {
|
||||
fn fetch<'a>(&self, user: &'a ArcUser, path: &'a [&str], create: bool) -> BoxFuture<'a, Result<Box<dyn DavNode>>> {
|
||||
if path.len() == 0 {
|
||||
let node = Box::new(self.clone()) as Box<dyn DavNode>;
|
||||
return async { Ok(node) }.boxed()
|
||||
}
|
||||
|
||||
if path[0] == "calendar" {
|
||||
return async move {
|
||||
let child = Box::new(CalendarListNode::new(user).await?);
|
||||
child.fetch(user, &path[1..], create).await
|
||||
}.boxed();
|
||||
}
|
||||
|
||||
//@NOTE: we can't create a node at this level
|
||||
async { Err(anyhow!("Not found")) }.boxed()
|
||||
}
|
||||
|
||||
fn children<'a>(&self, user: &'a ArcUser) -> BoxFuture<'a, Vec<Box<dyn DavNode>>> {
|
||||
async {
|
||||
CalendarListNode::new(user).await
|
||||
.map(|c| vec![Box::new(c) as Box<dyn DavNode>])
|
||||
.unwrap_or(vec![])
|
||||
}.boxed()
|
||||
}
|
||||
|
||||
fn path(&self, user: &ArcUser) -> String {
|
||||
format!("/{}/", user.username)
|
||||
}
|
||||
|
||||
fn supported_properties(&self, user: &ArcUser) -> dav::PropName<All> {
|
||||
dav::PropName(vec![
|
||||
dav::PropertyRequest::DisplayName,
|
||||
dav::PropertyRequest::ResourceType,
|
||||
dav::PropertyRequest::GetContentType,
|
||||
dav::PropertyRequest::Extension(all::PropertyRequest::Cal(cal::PropertyRequest::CalendarHomeSet)),
|
||||
])
|
||||
}
|
||||
fn properties(&self, user: &ArcUser, prop: dav::PropName<All>) -> PropertyStream<'static> {
|
||||
let user = user.clone();
|
||||
|
||||
futures::stream::iter(prop.0).map(move |n| {
|
||||
let prop = match n {
|
||||
dav::PropertyRequest::DisplayName => dav::Property::DisplayName(format!("{} home", user.username)),
|
||||
dav::PropertyRequest::ResourceType => dav::Property::ResourceType(vec![
|
||||
dav::ResourceType::Collection,
|
||||
dav::ResourceType::Extension(all::ResourceType::Acl(acl::ResourceType::Principal)),
|
||||
]),
|
||||
dav::PropertyRequest::GetContentType => dav::Property::GetContentType("httpd/unix-directory".into()),
|
||||
dav::PropertyRequest::Extension(all::PropertyRequest::Cal(cal::PropertyRequest::CalendarHomeSet)) =>
|
||||
dav::Property::Extension(all::Property::Cal(cal::Property::CalendarHomeSet(dav::Href(
|
||||
//@FIXME we are hardcoding the calendar path, instead we would want to use
|
||||
//objects
|
||||
format!("/{}/calendar/", user.username)
|
||||
)))),
|
||||
v => return Err(v),
|
||||
};
|
||||
Ok(prop)
|
||||
}).boxed()
|
||||
}
|
||||
|
||||
fn put<'a>(&'a self, _policy: PutPolicy, stream: Content<'a>) -> BoxFuture<'a, std::result::Result<Etag, std::io::Error>> {
|
||||
futures::future::err(std::io::Error::from(std::io::ErrorKind::Unsupported)).boxed()
|
||||
}
|
||||
|
||||
fn content(&self) -> Content<'static> {
|
||||
futures::stream::once(futures::future::err(std::io::Error::from(std::io::ErrorKind::Unsupported))).boxed()
|
||||
}
|
||||
|
||||
fn content_type(&self) -> &str {
|
||||
"text/plain"
|
||||
}
|
||||
|
||||
fn etag(&self) -> BoxFuture<Option<Etag>> {
|
||||
async { None }.boxed()
|
||||
}
|
||||
|
||||
fn delete(&self) -> BoxFuture<std::result::Result<(), std::io::Error>> {
|
||||
async { Err(std::io::Error::from(std::io::ErrorKind::PermissionDenied)) }.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct CalendarListNode {
|
||||
list: Vec<String>,
|
||||
}
|
||||
impl CalendarListNode {
|
||||
async fn new(user: &ArcUser) -> Result<Self> {
|
||||
let list = user.calendars.list(user).await?;
|
||||
Ok(Self { list })
|
||||
}
|
||||
}
|
||||
impl DavNode for CalendarListNode {
|
||||
fn fetch<'a>(&self, user: &'a ArcUser, path: &'a [&str], create: bool) -> BoxFuture<'a, Result<Box<dyn DavNode>>> {
|
||||
if path.len() == 0 {
|
||||
let node = Box::new(self.clone()) as Box<dyn DavNode>;
|
||||
return async { Ok(node) }.boxed();
|
||||
}
|
||||
|
||||
async move {
|
||||
//@FIXME: we should create a node if the open returns a "not found".
|
||||
let cal = user.calendars.open(user, path[0]).await?.ok_or(anyhow!("Not found"))?;
|
||||
let child = Box::new(CalendarNode {
|
||||
col: cal,
|
||||
calname: path[0].to_string()
|
||||
});
|
||||
child.fetch(user, &path[1..], create).await
|
||||
}.boxed()
|
||||
}
|
||||
|
||||
fn children<'a>(&self, user: &'a ArcUser) -> BoxFuture<'a, Vec<Box<dyn DavNode>>> {
|
||||
let list = self.list.clone();
|
||||
async move {
|
||||
//@FIXME maybe we want to be lazy here?!
|
||||
futures::stream::iter(list.iter())
|
||||
.filter_map(|name| async move {
|
||||
user.calendars.open(user, name).await
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|v| (name, v))
|
||||
})
|
||||
.map(|(name, cal)| Box::new(CalendarNode {
|
||||
col: cal,
|
||||
calname: name.to_string(),
|
||||
}) as Box<dyn DavNode>)
|
||||
.collect::<Vec<Box<dyn DavNode>>>()
|
||||
.await
|
||||
}.boxed()
|
||||
}
|
||||
|
||||
fn path(&self, user: &ArcUser) -> String {
|
||||
format!("/{}/calendar/", user.username)
|
||||
}
|
||||
|
||||
fn supported_properties(&self, user: &ArcUser) -> dav::PropName<All> {
|
||||
dav::PropName(vec![
|
||||
dav::PropertyRequest::DisplayName,
|
||||
dav::PropertyRequest::ResourceType,
|
||||
dav::PropertyRequest::GetContentType,
|
||||
])
|
||||
}
|
||||
fn properties(&self, user: &ArcUser, prop: dav::PropName<All>) -> PropertyStream<'static> {
|
||||
let user = user.clone();
|
||||
|
||||
futures::stream::iter(prop.0).map(move |n| {
|
||||
let prop = match n {
|
||||
dav::PropertyRequest::DisplayName => dav::Property::DisplayName(format!("{} calendars", user.username)),
|
||||
dav::PropertyRequest::ResourceType => dav::Property::ResourceType(vec![dav::ResourceType::Collection]),
|
||||
dav::PropertyRequest::GetContentType => dav::Property::GetContentType("httpd/unix-directory".into()),
|
||||
v => return Err(v),
|
||||
};
|
||||
Ok(prop)
|
||||
}).boxed()
|
||||
}
|
||||
|
||||
fn put<'a>(&'a self, _policy: PutPolicy, stream: Content<'a>) -> BoxFuture<'a, std::result::Result<Etag, std::io::Error>> {
|
||||
futures::future::err(std::io::Error::from(std::io::ErrorKind::Unsupported)).boxed()
|
||||
}
|
||||
|
||||
fn content(&self) -> Content<'static> {
|
||||
futures::stream::once(futures::future::err(std::io::Error::from(std::io::ErrorKind::Unsupported))).boxed()
|
||||
}
|
||||
|
||||
fn content_type(&self) -> &str {
|
||||
"text/plain"
|
||||
}
|
||||
|
||||
fn etag(&self) -> BoxFuture<Option<Etag>> {
|
||||
async { None }.boxed()
|
||||
}
|
||||
|
||||
fn delete(&self) -> BoxFuture<std::result::Result<(), std::io::Error>> {
|
||||
async { Err(std::io::Error::from(std::io::ErrorKind::PermissionDenied)) }.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct CalendarNode {
|
||||
col: Arc<Calendar>,
|
||||
calname: String,
|
||||
}
|
||||
impl DavNode for CalendarNode {
|
||||
fn fetch<'a>(&self, user: &'a ArcUser, path: &'a [&str], create: bool) -> BoxFuture<'a, Result<Box<dyn DavNode>>> {
|
||||
if path.len() == 0 {
|
||||
let node = Box::new(self.clone()) as Box<dyn DavNode>;
|
||||
return async { Ok(node) }.boxed()
|
||||
}
|
||||
|
||||
let col = self.col.clone();
|
||||
let calname = self.calname.clone();
|
||||
async move {
|
||||
match (col.dag().await.idx_by_filename.get(path[0]), create) {
|
||||
(Some(blob_id), _) => {
|
||||
let child = Box::new(EventNode {
|
||||
col: col.clone(),
|
||||
calname,
|
||||
filename: path[0].to_string(),
|
||||
blob_id: *blob_id,
|
||||
});
|
||||
child.fetch(user, &path[1..], create).await
|
||||
},
|
||||
(None, true) => {
|
||||
let child = Box::new(CreateEventNode {
|
||||
col: col.clone(),
|
||||
calname,
|
||||
filename: path[0].to_string(),
|
||||
});
|
||||
child.fetch(user, &path[1..], create).await
|
||||
},
|
||||
_ => Err(anyhow!("Not found")),
|
||||
}
|
||||
|
||||
}.boxed()
|
||||
}
|
||||
|
||||
fn children<'a>(&self, user: &'a ArcUser) -> BoxFuture<'a, Vec<Box<dyn DavNode>>> {
|
||||
let col = self.col.clone();
|
||||
let calname = self.calname.clone();
|
||||
|
||||
async move {
|
||||
col.dag().await.idx_by_filename.iter().map(|(filename, blob_id)| {
|
||||
Box::new(EventNode {
|
||||
col: col.clone(),
|
||||
calname: calname.clone(),
|
||||
filename: filename.to_string(),
|
||||
blob_id: *blob_id,
|
||||
}) as Box<dyn DavNode>
|
||||
}).collect()
|
||||
}.boxed()
|
||||
}
|
||||
|
||||
fn path(&self, user: &ArcUser) -> String {
|
||||
format!("/{}/calendar/{}/", user.username, self.calname)
|
||||
}
|
||||
|
||||
fn supported_properties(&self, user: &ArcUser) -> dav::PropName<All> {
|
||||
dav::PropName(vec![
|
||||
dav::PropertyRequest::DisplayName,
|
||||
dav::PropertyRequest::ResourceType,
|
||||
dav::PropertyRequest::GetContentType,
|
||||
dav::PropertyRequest::Extension(all::PropertyRequest::Cal(cal::PropertyRequest::SupportedCalendarComponentSet)),
|
||||
])
|
||||
}
|
||||
fn properties(&self, _user: &ArcUser, prop: dav::PropName<All>) -> PropertyStream<'static> {
|
||||
let calname = self.calname.to_string();
|
||||
|
||||
futures::stream::iter(prop.0).map(move |n| {
|
||||
let prop = match n {
|
||||
dav::PropertyRequest::DisplayName => dav::Property::DisplayName(format!("{} calendar", calname)),
|
||||
dav::PropertyRequest::ResourceType => dav::Property::ResourceType(vec![
|
||||
dav::ResourceType::Collection,
|
||||
dav::ResourceType::Extension(all::ResourceType::Cal(cal::ResourceType::Calendar)),
|
||||
]),
|
||||
//dav::PropertyRequest::GetContentType => dav::AnyProperty::Value(dav::Property::GetContentType("httpd/unix-directory".into())),
|
||||
//@FIXME seems wrong but seems to be what Thunderbird expects...
|
||||
dav::PropertyRequest::GetContentType => dav::Property::GetContentType("text/calendar".into()),
|
||||
dav::PropertyRequest::Extension(all::PropertyRequest::Cal(cal::PropertyRequest::SupportedCalendarComponentSet))
|
||||
=> dav::Property::Extension(all::Property::Cal(cal::Property::SupportedCalendarComponentSet(vec![
|
||||
cal::CompSupport(cal::Component::VEvent),
|
||||
]))),
|
||||
v => return Err(v),
|
||||
};
|
||||
Ok(prop)
|
||||
}).boxed()
|
||||
}
|
||||
|
||||
fn put<'a>(&'a self, _policy: PutPolicy, _stream: Content<'a>) -> BoxFuture<'a, std::result::Result<Etag, std::io::Error>> {
|
||||
futures::future::err(std::io::Error::from(std::io::ErrorKind::Unsupported)).boxed()
|
||||
}
|
||||
|
||||
fn content<'a>(&'a self) -> Content<'static> {
|
||||
futures::stream::once(futures::future::err(std::io::Error::from(std::io::ErrorKind::Unsupported))).boxed()
|
||||
}
|
||||
|
||||
fn content_type(&self) -> &str {
|
||||
"text/plain"
|
||||
}
|
||||
|
||||
fn etag(&self) -> BoxFuture<Option<Etag>> {
|
||||
async { None }.boxed()
|
||||
}
|
||||
|
||||
fn delete(&self) -> BoxFuture<std::result::Result<(), std::io::Error>> {
|
||||
async { Err(std::io::Error::from(std::io::ErrorKind::PermissionDenied)) }.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct EventNode {
|
||||
col: Arc<Calendar>,
|
||||
calname: String,
|
||||
filename: String,
|
||||
blob_id: BlobId,
|
||||
}
|
||||
|
||||
impl DavNode for EventNode {
|
||||
fn fetch<'a>(&self, user: &'a ArcUser, path: &'a [&str], create: bool) -> BoxFuture<'a, Result<Box<dyn DavNode>>> {
|
||||
if path.len() == 0 {
|
||||
let node = Box::new(self.clone()) as Box<dyn DavNode>;
|
||||
return async { Ok(node) }.boxed()
|
||||
}
|
||||
|
||||
async { Err(anyhow!("Not supported: can't create a child on an event node")) }.boxed()
|
||||
}
|
||||
|
||||
fn children<'a>(&self, user: &'a ArcUser) -> BoxFuture<'a, Vec<Box<dyn DavNode>>> {
|
||||
async { vec![] }.boxed()
|
||||
}
|
||||
|
||||
fn path(&self, user: &ArcUser) -> String {
|
||||
format!("/{}/calendar/{}/{}", user.username, self.calname, self.filename)
|
||||
}
|
||||
|
||||
fn supported_properties(&self, user: &ArcUser) -> dav::PropName<All> {
|
||||
dav::PropName(vec![
|
||||
dav::PropertyRequest::DisplayName,
|
||||
dav::PropertyRequest::ResourceType,
|
||||
dav::PropertyRequest::GetEtag,
|
||||
dav::PropertyRequest::Extension(all::PropertyRequest::Cal(cal::PropertyRequest::CalendarData(cal::CalendarDataRequest::default()))),
|
||||
])
|
||||
}
|
||||
fn properties(&self, _user: &ArcUser, prop: dav::PropName<All>) -> PropertyStream<'static> {
|
||||
let this = self.clone();
|
||||
|
||||
futures::stream::iter(prop.0).then(move |n| {
|
||||
let this = this.clone();
|
||||
|
||||
async move {
|
||||
let prop = match &n {
|
||||
dav::PropertyRequest::DisplayName => dav::Property::DisplayName(format!("{} event", this.filename)),
|
||||
dav::PropertyRequest::ResourceType => dav::Property::ResourceType(vec![]),
|
||||
dav::PropertyRequest::GetContentType => dav::Property::GetContentType("text/calendar".into()),
|
||||
dav::PropertyRequest::GetEtag => {
|
||||
let etag = this.etag().await.ok_or(n.clone())?;
|
||||
dav::Property::GetEtag(etag)
|
||||
},
|
||||
dav::PropertyRequest::Extension(all::PropertyRequest::Cal(cal::PropertyRequest::CalendarData(_req))) => {
|
||||
let ics = String::from_utf8(this.col.get(this.blob_id).await.or(Err(n.clone()))?).or(Err(n.clone()))?;
|
||||
|
||||
dav::Property::Extension(all::Property::Cal(cal::Property::CalendarData(cal::CalendarDataPayload {
|
||||
mime: None,
|
||||
payload: ics,
|
||||
})))
|
||||
},
|
||||
_ => return Err(n),
|
||||
};
|
||||
Ok(prop)
|
||||
}
|
||||
}).boxed()
|
||||
}
|
||||
|
||||
fn put<'a>(&'a self, policy: PutPolicy, stream: Content<'a>) -> BoxFuture<'a, std::result::Result<Etag, std::io::Error>> {
|
||||
async {
|
||||
let existing_etag = self.etag().await.ok_or(std::io::Error::new(std::io::ErrorKind::Other, "Etag error"))?;
|
||||
match policy {
|
||||
PutPolicy::CreateOnly => return Err(std::io::Error::from(std::io::ErrorKind::AlreadyExists)),
|
||||
PutPolicy::ReplaceEtag(etag) if etag != existing_etag.as_str() => return Err(std::io::Error::from(std::io::ErrorKind::AlreadyExists)),
|
||||
_ => ()
|
||||
};
|
||||
|
||||
//@FIXME for now, our storage interface does not allow streaming,
|
||||
// so we load everything in memory
|
||||
let mut evt = Vec::new();
|
||||
let mut reader = stream.into_async_read();
|
||||
reader.read_to_end(&mut evt).await.or(Err(std::io::Error::from(std::io::ErrorKind::BrokenPipe)))?;
|
||||
let (_token, entry) = self.col.put(self.filename.as_str(), evt.as_ref()).await.or(Err(std::io::ErrorKind::Interrupted))?;
|
||||
self.col.opportunistic_sync().await.or(Err(std::io::ErrorKind::ConnectionReset))?;
|
||||
Ok(entry.2)
|
||||
}.boxed()
|
||||
}
|
||||
|
||||
fn content<'a>(&'a self) -> Content<'static> {
|
||||
//@FIXME for now, our storage interface does not allow streaming,
|
||||
// so we load everything in memory
|
||||
let calendar = self.col.clone();
|
||||
let blob_id = self.blob_id.clone();
|
||||
let r = async move {
|
||||
let content = calendar.get(blob_id).await.or(Err(std::io::Error::from(std::io::ErrorKind::Interrupted)));
|
||||
Ok(hyper::body::Bytes::from(content?))
|
||||
};
|
||||
futures::stream::once(Box::pin(r)).boxed()
|
||||
}
|
||||
|
||||
fn content_type(&self) -> &str {
|
||||
"text/calendar"
|
||||
}
|
||||
|
||||
fn etag(&self) -> BoxFuture<Option<Etag>> {
|
||||
let calendar = self.col.clone();
|
||||
|
||||
async move {
|
||||
calendar.dag().await.table.get(&self.blob_id).map(|(_, _, etag)| etag.to_string())
|
||||
}.boxed()
|
||||
}
|
||||
|
||||
fn delete(&self) -> BoxFuture<std::result::Result<(), std::io::Error>> {
|
||||
let calendar = self.col.clone();
|
||||
let blob_id = self.blob_id.clone();
|
||||
|
||||
async move {
|
||||
let _token = match calendar.delete(blob_id).await {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
tracing::error!(err=?e, "delete event node");
|
||||
return Err(std::io::Error::from(std::io::ErrorKind::Interrupted))
|
||||
},
|
||||
};
|
||||
calendar.opportunistic_sync().await.or(Err(std::io::ErrorKind::ConnectionReset))?;
|
||||
Ok(())
|
||||
}.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct CreateEventNode {
|
||||
col: Arc<Calendar>,
|
||||
calname: String,
|
||||
filename: String,
|
||||
}
|
||||
impl DavNode for CreateEventNode {
|
||||
fn fetch<'a>(&self, user: &'a ArcUser, path: &'a [&str], create: bool) -> BoxFuture<'a, Result<Box<dyn DavNode>>> {
|
||||
if path.len() == 0 {
|
||||
let node = Box::new(self.clone()) as Box<dyn DavNode>;
|
||||
return async { Ok(node) }.boxed()
|
||||
}
|
||||
|
||||
async { Err(anyhow!("Not supported: can't create a child on an event node")) }.boxed()
|
||||
}
|
||||
|
||||
fn children<'a>(&self, user: &'a ArcUser) -> BoxFuture<'a, Vec<Box<dyn DavNode>>> {
|
||||
async { vec![] }.boxed()
|
||||
}
|
||||
|
||||
fn path(&self, user: &ArcUser) -> String {
|
||||
format!("/{}/calendar/{}/{}", user.username, self.calname, self.filename)
|
||||
}
|
||||
|
||||
fn supported_properties(&self, user: &ArcUser) -> dav::PropName<All> {
|
||||
dav::PropName(vec![])
|
||||
}
|
||||
|
||||
fn properties(&self, _user: &ArcUser, prop: dav::PropName<All>) -> PropertyStream<'static> {
|
||||
futures::stream::iter(vec![]).boxed()
|
||||
}
|
||||
|
||||
fn put<'a>(&'a self, _policy: PutPolicy, stream: Content<'a>) -> BoxFuture<'a, std::result::Result<Etag, std::io::Error>> {
|
||||
//@NOTE: policy might not be needed here: whatever we put, there is no known entries here
|
||||
|
||||
async {
|
||||
//@FIXME for now, our storage interface does not allow for streaming
|
||||
let mut evt = Vec::new();
|
||||
let mut reader = stream.into_async_read();
|
||||
reader.read_to_end(&mut evt).await.unwrap();
|
||||
let (_token, entry) = self.col.put(self.filename.as_str(), evt.as_ref()).await.or(Err(std::io::ErrorKind::Interrupted))?;
|
||||
self.col.opportunistic_sync().await.or(Err(std::io::ErrorKind::ConnectionReset))?;
|
||||
Ok(entry.2)
|
||||
}.boxed()
|
||||
}
|
||||
|
||||
fn content(&self) -> Content<'static> {
|
||||
futures::stream::once(futures::future::err(std::io::Error::from(std::io::ErrorKind::Unsupported))).boxed()
|
||||
}
|
||||
|
||||
fn content_type(&self) -> &str {
|
||||
"text/plain"
|
||||
}
|
||||
|
||||
fn etag(&self) -> BoxFuture<Option<Etag>> {
|
||||
async { None }.boxed()
|
||||
}
|
||||
|
||||
fn delete(&self) -> BoxFuture<std::result::Result<(), std::io::Error>> {
|
||||
// Nothing to delete
|
||||
async { Ok(()) }.boxed()
|
||||
}
|
||||
}
|
|
@ -4,12 +4,13 @@ use imap_codec::imap_types::core::AString;
|
|||
use imap_codec::imap_types::response::Code;
|
||||
use imap_codec::imap_types::secret::Secret;
|
||||
|
||||
use aero_user::login::ArcLoginProvider;
|
||||
use aero_collections::user::User;
|
||||
|
||||
use crate::imap::capability::ServerCapability;
|
||||
use crate::imap::command::anystate;
|
||||
use crate::imap::flow;
|
||||
use crate::imap::response::Response;
|
||||
use crate::login::ArcLoginProvider;
|
||||
use crate::mail::user::User;
|
||||
|
||||
//--- dispatching
|
||||
|
|
@ -14,16 +14,16 @@ use imap_codec::imap_types::mailbox::{ListMailbox, Mailbox as MailboxCodec};
|
|||
use imap_codec::imap_types::response::{Code, CodeOther, Data};
|
||||
use imap_codec::imap_types::status::{StatusDataItem, StatusDataItemName};
|
||||
|
||||
use aero_collections::mail::uidindex::*;
|
||||
use aero_collections::user::User;
|
||||
use aero_collections::mail::IMF;
|
||||
use aero_collections::mail::namespace::MAILBOX_HIERARCHY_DELIMITER as MBX_HIER_DELIM_RAW;
|
||||
|
||||
use crate::imap::capability::{ClientCapability, ServerCapability};
|
||||
use crate::imap::command::{anystate, MailboxName};
|
||||
use crate::imap::flow;
|
||||
use crate::imap::mailbox_view::{MailboxView, UpdateParameters};
|
||||
use crate::imap::mailbox_view::MailboxView;
|
||||
use crate::imap::response::Response;
|
||||
use crate::imap::Body;
|
||||
|
||||
use crate::mail::uidindex::*;
|
||||
use crate::mail::user::{User, MAILBOX_HIERARCHY_DELIMITER as MBX_HIER_DELIM_RAW};
|
||||
use crate::mail::IMF;
|
||||
|
||||
pub struct AuthenticatedContext<'a> {
|
||||
pub req: &'a Command<'static>,
|
||||
|
@ -610,7 +610,7 @@ impl<'a> AuthenticatedContext<'a> {
|
|||
Some(mb) => mb,
|
||||
None => bail!("Mailbox does not exist"),
|
||||
};
|
||||
let mut view = MailboxView::new(mb, self.client_capabilities.condstore.is_enabled()).await;
|
||||
let view = MailboxView::new(mb, self.client_capabilities.condstore.is_enabled()).await;
|
||||
|
||||
if date.is_some() {
|
||||
tracing::warn!("Cannot set date when appending message");
|
|
@ -3,7 +3,7 @@ pub mod anystate;
|
|||
pub mod authenticated;
|
||||
pub mod selected;
|
||||
|
||||
use crate::mail::user::INBOX;
|
||||
use aero_collections::mail::namespace::INBOX;
|
||||
use imap_codec::imap_types::mailbox::Mailbox as MailboxCodec;
|
||||
|
||||
/// Convert an IMAP mailbox name/identifier representation
|
|
@ -3,7 +3,7 @@ use std::sync::Arc;
|
|||
|
||||
use anyhow::Result;
|
||||
use imap_codec::imap_types::command::{Command, CommandBody, FetchModifier, StoreModifier};
|
||||
use imap_codec::imap_types::core::{Charset, Vec1};
|
||||
use imap_codec::imap_types::core::Charset;
|
||||
use imap_codec::imap_types::fetch::MacroOrMessageDataItemNames;
|
||||
use imap_codec::imap_types::flag::{Flag, StoreResponse, StoreType};
|
||||
use imap_codec::imap_types::mailbox::Mailbox as MailboxCodec;
|
||||
|
@ -11,13 +11,14 @@ use imap_codec::imap_types::response::{Code, CodeOther};
|
|||
use imap_codec::imap_types::search::SearchKey;
|
||||
use imap_codec::imap_types::sequence::SequenceSet;
|
||||
|
||||
use aero_collections::user::User;
|
||||
|
||||
use crate::imap::attributes::AttributesProxy;
|
||||
use crate::imap::capability::{ClientCapability, ServerCapability};
|
||||
use crate::imap::command::{anystate, authenticated, MailboxName};
|
||||
use crate::imap::flow;
|
||||
use crate::imap::mailbox_view::{MailboxView, UpdateParameters};
|
||||
use crate::imap::response::Response;
|
||||
use crate::mail::user::User;
|
||||
|
||||
pub struct SelectedContext<'a> {
|
||||
pub req: &'a Command<'static>,
|
|
@ -5,8 +5,9 @@ use std::sync::Arc;
|
|||
use imap_codec::imap_types::core::Tag;
|
||||
use tokio::sync::Notify;
|
||||
|
||||
use aero_collections::user::User;
|
||||
|
||||
use crate::imap::mailbox_view::MailboxView;
|
||||
use crate::mail::user::User;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
|
@ -3,8 +3,8 @@ use std::num::{NonZeroU32, NonZeroU64};
|
|||
use anyhow::{anyhow, Result};
|
||||
use imap_codec::imap_types::sequence::{SeqOrUid, Sequence, SequenceSet};
|
||||
|
||||
use crate::mail::uidindex::{ImapUid, ModSeq, UidIndex};
|
||||
use crate::mail::unique_ident::UniqueIdent;
|
||||
use aero_collections::mail::uidindex::{ImapUid, ModSeq, UidIndex};
|
||||
use aero_collections::unique_ident::UniqueIdent;
|
||||
|
||||
pub struct Index<'a> {
|
||||
pub imap_index: Vec<MailIndex<'a>>,
|
|
@ -16,7 +16,7 @@ use eml_codec::{
|
|||
part::{composite::Message, AnyPart},
|
||||
};
|
||||
|
||||
use crate::mail::query::QueryResult;
|
||||
use aero_collections::mail::query::QueryResult;
|
||||
|
||||
use crate::imap::attributes::AttributesProxy;
|
||||
use crate::imap::flags;
|
|
@ -6,18 +6,18 @@ use anyhow::{anyhow, Error, Result};
|
|||
|
||||
use futures::stream::{StreamExt, TryStreamExt};
|
||||
|
||||
use imap_codec::imap_types::core::{Charset, Vec1};
|
||||
use imap_codec::imap_types::core::Charset;
|
||||
use imap_codec::imap_types::fetch::MessageDataItem;
|
||||
use imap_codec::imap_types::flag::{Flag, FlagFetch, FlagPerm, StoreResponse, StoreType};
|
||||
use imap_codec::imap_types::response::{Code, CodeOther, Data, Status};
|
||||
use imap_codec::imap_types::search::SearchKey;
|
||||
use imap_codec::imap_types::sequence::SequenceSet;
|
||||
|
||||
use crate::mail::mailbox::Mailbox;
|
||||
use crate::mail::query::QueryScope;
|
||||
use crate::mail::snapshot::FrozenMailbox;
|
||||
use crate::mail::uidindex::{ImapUid, ImapUidvalidity, ModSeq};
|
||||
use crate::mail::unique_ident::UniqueIdent;
|
||||
use aero_collections::mail::mailbox::Mailbox;
|
||||
use aero_collections::mail::query::QueryScope;
|
||||
use aero_collections::mail::snapshot::FrozenMailbox;
|
||||
use aero_collections::mail::uidindex::{ImapUid, ImapUidvalidity, ModSeq};
|
||||
use aero_collections::unique_ident::UniqueIdent;
|
||||
|
||||
use crate::imap::attributes::AttributesProxy;
|
||||
use crate::imap::flags;
|
|
@ -384,6 +384,8 @@ impl<'a> NodeMsg<'a> {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
struct NodeMult<'a>(&'a NodeMime<'a>, &'a composite::Multipart<'a>);
|
||||
impl<'a> NodeMult<'a> {
|
||||
fn structure(&self, is_ext: bool) -> Result<BodyStructure<'static>> {
|
|
@ -15,13 +15,11 @@ mod session;
|
|||
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use futures::stream::{FuturesUnordered, StreamExt};
|
||||
|
||||
use tokio::net::TcpListener;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::sync::watch;
|
||||
|
||||
use imap_codec::imap_types::response::{Code, CommandContinuationRequest, Response, Status};
|
||||
use imap_codec::imap_types::{core::Text, response::Greeting};
|
||||
use imap_flow::server::{ServerFlow, ServerFlowEvent, ServerFlowOptions};
|
||||
|
@ -29,12 +27,13 @@ use imap_flow::stream::AnyStream;
|
|||
use rustls_pemfile::{certs, private_key};
|
||||
use tokio_rustls::TlsAcceptor;
|
||||
|
||||
use crate::config::{ImapConfig, ImapUnsecureConfig};
|
||||
use aero_user::config::{ImapConfig, ImapUnsecureConfig};
|
||||
use aero_user::login::ArcLoginProvider;
|
||||
|
||||
use crate::imap::capability::ServerCapability;
|
||||
use crate::imap::request::Request;
|
||||
use crate::imap::response::{Body, ResponseOrIdle};
|
||||
use crate::imap::session::Instance;
|
||||
use crate::login::ArcLoginProvider;
|
||||
|
||||
/// Server is a thin wrapper to register our Services in BàL
|
||||
pub struct Server {
|
||||
|
@ -140,7 +139,6 @@ impl Server {
|
|||
use std::sync::Arc;
|
||||
use tokio::sync::mpsc::*;
|
||||
use tokio::sync::Notify;
|
||||
use tokio_util::bytes::BytesMut;
|
||||
|
||||
const PIPELINABLE_COMMANDS: usize = 64;
|
||||
|
||||
|
@ -325,8 +323,6 @@ impl NetLoop {
|
|||
self.server.enqueue_status(Status::bye(None, "Internal session exited").unwrap());
|
||||
tracing::error!("session task exited for {:?}, quitting", self.ctx.addr);
|
||||
},
|
||||
Some(_) => unreachable!(),
|
||||
|
||||
},
|
||||
|
||||
// When receiving a CTRL+C
|
||||
|
@ -337,85 +333,4 @@ impl NetLoop {
|
|||
};
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
async fn idle_mode(&mut self, mut buff: BytesMut, stop: Arc<Notify>) -> Result<LoopMode> {
|
||||
// Flush send
|
||||
loop {
|
||||
tracing::trace!("flush server send");
|
||||
match self.server.progress_send().await? {
|
||||
Some(..) => continue,
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
|
||||
tokio::select! {
|
||||
// Receiving IDLE event from background
|
||||
maybe_msg = self.resp_rx.recv() => match maybe_msg {
|
||||
// Session decided idle is terminated
|
||||
Some(ResponseOrIdle::Response(response)) => {
|
||||
tracing::trace!("server imap session said idle is done, sending response done, switching to interactive");
|
||||
for body_elem in response.body.into_iter() {
|
||||
let _handle = match body_elem {
|
||||
Body::Data(d) => self.server.enqueue_data(d),
|
||||
Body::Status(s) => self.server.enqueue_status(s),
|
||||
};
|
||||
}
|
||||
self.server.enqueue_status(response.completion);
|
||||
return Ok(LoopMode::Interactive)
|
||||
},
|
||||
// Session has some information for user
|
||||
Some(ResponseOrIdle::IdleEvent(elems)) => {
|
||||
tracing::trace!("server imap session has some change to communicate to the client");
|
||||
for body_elem in elems.into_iter() {
|
||||
let _handle = match body_elem {
|
||||
Body::Data(d) => self.server.enqueue_data(d),
|
||||
Body::Status(s) => self.server.enqueue_status(s),
|
||||
};
|
||||
}
|
||||
self.cmd_tx.try_send(Request::Idle)?;
|
||||
return Ok(LoopMode::Idle(buff, stop))
|
||||
},
|
||||
|
||||
// Session crashed
|
||||
None => {
|
||||
self.server.enqueue_status(Status::bye(None, "Internal session exited").unwrap());
|
||||
tracing::error!("session task exited for {:?}, quitting", self.ctx.addr);
|
||||
return Ok(LoopMode::Interactive)
|
||||
},
|
||||
|
||||
// Session can't start idling while already idling, it's a logic error!
|
||||
Some(ResponseOrIdle::StartIdle(..)) => bail!("can't start idling while already idling!"),
|
||||
},
|
||||
|
||||
// User is trying to interact with us
|
||||
read_client_result = self.server.stream.read(&mut buff) => {
|
||||
let _bytes_read = read_client_result?;
|
||||
use imap_codec::decode::Decoder;
|
||||
let codec = imap_codec::IdleDoneCodec::new();
|
||||
tracing::trace!("client sent some data for the server IMAP session");
|
||||
match codec.decode(&buff) {
|
||||
Ok(([], imap_codec::imap_types::extensions::idle::IdleDone)) => {
|
||||
// Session will be informed that it must stop idle
|
||||
// It will generate the "done" message and change the loop mode
|
||||
tracing::trace!("client sent DONE and want to stop IDLE");
|
||||
stop.notify_one()
|
||||
},
|
||||
Err(_) => {
|
||||
tracing::trace!("Unable to decode DONE, maybe not enough data were sent?");
|
||||
},
|
||||
_ => bail!("Client sent data after terminating the continuation without waiting for the server. This is an unsupported behavior and bug in Aerogramme, quitting."),
|
||||
};
|
||||
|
||||
return Ok(LoopMode::Idle(buff, stop))
|
||||
},
|
||||
|
||||
// When receiving a CTRL+C
|
||||
_ = self.ctx.must_exit.changed() => {
|
||||
tracing::trace!("CTRL+C sent, aborting IDLE for this session");
|
||||
self.server.enqueue_status(Status::bye(None, "Server is being shutdown").unwrap());
|
||||
return Ok(LoopMode::Interactive)
|
||||
},
|
||||
};
|
||||
}*/
|
||||
}
|
|
@ -4,9 +4,10 @@ use imap_codec::imap_types::core::Vec1;
|
|||
use imap_codec::imap_types::search::{MetadataItemSearch, SearchKey};
|
||||
use imap_codec::imap_types::sequence::{SeqOrUid, Sequence, SequenceSet};
|
||||
|
||||
use aero_collections::mail::query::QueryScope;
|
||||
|
||||
use crate::imap::index::MailIndex;
|
||||
use crate::imap::mail_view::MailView;
|
||||
use crate::mail::query::QueryScope;
|
||||
|
||||
pub enum SeqType {
|
||||
Undefined,
|
|
@ -1,11 +1,13 @@
|
|||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use imap_codec::imap_types::{command::Command, core::Tag};
|
||||
|
||||
use aero_user::login::ArcLoginProvider;
|
||||
|
||||
use crate::imap::capability::{ClientCapability, ServerCapability};
|
||||
use crate::imap::command::{anonymous, authenticated, selected};
|
||||
use crate::imap::flow;
|
||||
use crate::imap::request::Request;
|
||||
use crate::imap::response::{Response, ResponseOrIdle};
|
||||
use crate::login::ArcLoginProvider;
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use imap_codec::imap_types::{command::Command, core::Tag};
|
||||
|
||||
//-----
|
||||
pub struct Instance {
|
|
@ -0,0 +1,6 @@
|
|||
#![feature(async_closure)]
|
||||
|
||||
pub mod dav;
|
||||
pub mod imap;
|
||||
pub mod lmtp;
|
||||
pub mod sasl;
|
|
@ -10,18 +10,16 @@ use futures::{
|
|||
stream::{FuturesOrdered, FuturesUnordered},
|
||||
StreamExt,
|
||||
};
|
||||
use log::*;
|
||||
use tokio::net::TcpListener;
|
||||
use tokio::select;
|
||||
use tokio::sync::watch;
|
||||
use tokio_util::compat::*;
|
||||
|
||||
use smtp_message::{DataUnescaper, Email, EscapedDataReader, Reply, ReplyCode};
|
||||
use smtp_server::{reply, Config, ConnectionMetadata, Decision, MailMetadata};
|
||||
|
||||
use crate::config::*;
|
||||
use crate::login::*;
|
||||
use crate::mail::incoming::EncryptedMessage;
|
||||
use aero_user::config::*;
|
||||
use aero_user::login::*;
|
||||
use aero_collections::mail::incoming::EncryptedMessage;
|
||||
|
||||
pub struct LmtpServer {
|
||||
bind_addr: SocketAddr,
|
||||
|
@ -43,7 +41,7 @@ impl LmtpServer {
|
|||
|
||||
pub async fn run(self: &Arc<Self>, mut must_exit: watch::Receiver<bool>) -> Result<()> {
|
||||
let tcp = TcpListener::bind(self.bind_addr).await?;
|
||||
info!("LMTP server listening on {:#}", self.bind_addr);
|
||||
tracing::info!("LMTP server listening on {:#}", self.bind_addr);
|
||||
|
||||
let mut connections = FuturesUnordered::new();
|
||||
|
||||
|
@ -60,7 +58,7 @@ impl LmtpServer {
|
|||
_ = wait_conn_finished => continue,
|
||||
_ = must_exit.changed() => continue,
|
||||
};
|
||||
info!("LMTP: accepted connection from {}", remote_addr);
|
||||
tracing::info!("LMTP: accepted connection from {}", remote_addr);
|
||||
|
||||
let conn = tokio::spawn(smtp_server::interact(
|
||||
socket.compat(),
|
||||
|
@ -73,7 +71,7 @@ impl LmtpServer {
|
|||
}
|
||||
drop(tcp);
|
||||
|
||||
info!("LMTP server shutting down, draining remaining connections...");
|
||||
tracing::info!("LMTP server shutting down, draining remaining connections...");
|
||||
while connections.next().await.is_some() {}
|
||||
|
||||
Ok(())
|
|
@ -0,0 +1,142 @@
|
|||
use std::net::SocketAddr;
|
||||
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use futures::stream::{FuturesUnordered, StreamExt};
|
||||
use tokio::io::BufStream;
|
||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
|
||||
use tokio::net::{TcpListener, TcpStream};
|
||||
use tokio::sync::watch;
|
||||
use tokio_util::bytes::BytesMut;
|
||||
|
||||
use aero_user::config::AuthConfig;
|
||||
use aero_user::login::ArcLoginProvider;
|
||||
use aero_sasl::{flow::State, decode::client_command, encode::Encode};
|
||||
|
||||
pub struct AuthServer {
|
||||
login_provider: ArcLoginProvider,
|
||||
bind_addr: SocketAddr,
|
||||
}
|
||||
|
||||
impl AuthServer {
|
||||
pub fn new(config: AuthConfig, login_provider: ArcLoginProvider) -> Self {
|
||||
Self {
|
||||
bind_addr: config.bind_addr,
|
||||
login_provider,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run(self: Self, mut must_exit: watch::Receiver<bool>) -> Result<()> {
|
||||
let tcp = TcpListener::bind(self.bind_addr).await?;
|
||||
tracing::info!(
|
||||
"SASL Authentication Protocol listening on {:#}",
|
||||
self.bind_addr
|
||||
);
|
||||
|
||||
let mut connections = FuturesUnordered::new();
|
||||
|
||||
while !*must_exit.borrow() {
|
||||
let wait_conn_finished = async {
|
||||
if connections.is_empty() {
|
||||
futures::future::pending().await
|
||||
} else {
|
||||
connections.next().await
|
||||
}
|
||||
};
|
||||
|
||||
let (socket, remote_addr) = tokio::select! {
|
||||
a = tcp.accept() => a?,
|
||||
_ = wait_conn_finished => continue,
|
||||
_ = must_exit.changed() => continue,
|
||||
};
|
||||
|
||||
tracing::info!("AUTH: accepted connection from {}", remote_addr);
|
||||
let conn = tokio::spawn(
|
||||
NetLoop::new(socket, self.login_provider.clone(), must_exit.clone()).run_error(),
|
||||
);
|
||||
|
||||
connections.push(conn);
|
||||
}
|
||||
drop(tcp);
|
||||
|
||||
tracing::info!("AUTH server shutting down, draining remaining connections...");
|
||||
while connections.next().await.is_some() {}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct NetLoop {
|
||||
login: ArcLoginProvider,
|
||||
stream: BufStream<TcpStream>,
|
||||
stop: watch::Receiver<bool>,
|
||||
state: State,
|
||||
read_buf: Vec<u8>,
|
||||
write_buf: BytesMut,
|
||||
}
|
||||
|
||||
impl NetLoop {
|
||||
fn new(stream: TcpStream, login: ArcLoginProvider, stop: watch::Receiver<bool>) -> Self {
|
||||
Self {
|
||||
login,
|
||||
stream: BufStream::new(stream),
|
||||
state: State::Init,
|
||||
stop,
|
||||
read_buf: Vec::new(),
|
||||
write_buf: BytesMut::new(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_error(self) {
|
||||
match self.run().await {
|
||||
Ok(()) => tracing::info!("Auth session succeeded"),
|
||||
Err(e) => tracing::error!(err=?e, "Auth session failed"),
|
||||
}
|
||||
}
|
||||
|
||||
async fn run(mut self) -> Result<()> {
|
||||
loop {
|
||||
tokio::select! {
|
||||
read_res = self.stream.read_until(b'\n', &mut self.read_buf) => {
|
||||
// Detect EOF / socket close
|
||||
let bread = read_res?;
|
||||
if bread == 0 {
|
||||
tracing::info!("Reading buffer empty, connection has been closed. Exiting AUTH session.");
|
||||
return Ok(())
|
||||
}
|
||||
|
||||
// Parse command
|
||||
let (_, cmd) = client_command(&self.read_buf).map_err(|_| anyhow!("Unable to parse command"))?;
|
||||
tracing::trace!(cmd=?cmd, "Received command");
|
||||
|
||||
// Make some progress in our local state
|
||||
let login = async |user: String, pass: String| self.login.login(user.as_str(), pass.as_str()).await.is_ok();
|
||||
self.state.progress(cmd, login).await;
|
||||
if matches!(self.state, State::Error) {
|
||||
bail!("Internal state is in error, previous logs explain what went wrong");
|
||||
}
|
||||
|
||||
// Build response
|
||||
let srv_cmds = self.state.response();
|
||||
srv_cmds.iter().try_for_each(|r| {
|
||||
tracing::trace!(cmd=?r, "Sent command");
|
||||
r.encode(&mut self.write_buf)
|
||||
})?;
|
||||
|
||||
// Send responses if at least one command response has been generated
|
||||
if !srv_cmds.is_empty() {
|
||||
self.stream.write_all(&self.write_buf).await?;
|
||||
self.stream.flush().await?;
|
||||
}
|
||||
|
||||
// Reset buffers
|
||||
self.read_buf.clear();
|
||||
self.write_buf.clear();
|
||||
},
|
||||
_ = self.stop.changed() => {
|
||||
tracing::debug!("Server is stopping, quitting this runner");
|
||||
return Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
[package]
|
||||
name = "aero-sasl"
|
||||
version = "0.3.0"
|
||||
authors = ["Alex Auvolat <alex@adnab.me>", "Quentin Dufour <quentin@dufour.io>"]
|
||||
edition = "2021"
|
||||
license = "EUPL-1.2"
|
||||
description = "A partial and standalone implementation of the Dovecot SASL Auth Protocol"
|
||||
|
||||
[dependencies]
|
||||
|
||||
anyhow.workspace = true
|
||||
base64.workspace = true
|
||||
futures.workspace = true
|
||||
nom.workspace = true
|
||||
rand.workspace = true
|
||||
tokio.workspace = true
|
||||
tokio-util.workspace = true
|
||||
tracing.workspace = true
|
||||
hex.workspace = true
|
||||
|
||||
#log.workspace = true
|
||||
#serde.workspace = true
|
|
@ -0,0 +1,243 @@
|
|||
use base64::Engine;
|
||||
use nom::{
|
||||
branch::alt,
|
||||
bytes::complete::{tag, tag_no_case, take, take_while, take_while1},
|
||||
character::complete::{tab, u16, u64},
|
||||
combinator::{map, opt, recognize, rest, value},
|
||||
error::{Error, ErrorKind},
|
||||
multi::{many1, separated_list0},
|
||||
sequence::{pair, preceded, tuple},
|
||||
IResult,
|
||||
};
|
||||
|
||||
use super::types::*;
|
||||
|
||||
pub fn client_command<'a>(input: &'a [u8]) -> IResult<&'a [u8], ClientCommand> {
|
||||
alt((version_command, cpid_command, auth_command, cont_command))(input)
|
||||
}
|
||||
|
||||
/*
|
||||
fn server_command(buf: &u8) -> IResult<&u8, ServerCommand> {
|
||||
unimplemented!();
|
||||
}
|
||||
*/
|
||||
|
||||
// ---------------------
|
||||
|
||||
fn version_command<'a>(input: &'a [u8]) -> IResult<&'a [u8], ClientCommand> {
|
||||
let mut parser = tuple((tag_no_case(b"VERSION"), tab, u64, tab, u64));
|
||||
|
||||
let (input, (_, _, major, _, minor)) = parser(input)?;
|
||||
Ok((input, ClientCommand::Version(Version { major, minor })))
|
||||
}
|
||||
|
||||
pub fn cpid_command<'a>(input: &'a [u8]) -> IResult<&'a [u8], ClientCommand> {
|
||||
preceded(
|
||||
pair(tag_no_case(b"CPID"), tab),
|
||||
map(u64, |v| ClientCommand::Cpid(v)),
|
||||
)(input)
|
||||
}
|
||||
|
||||
fn mechanism<'a>(input: &'a [u8]) -> IResult<&'a [u8], Mechanism> {
|
||||
alt((
|
||||
value(Mechanism::Plain, tag_no_case(b"PLAIN")),
|
||||
value(Mechanism::Login, tag_no_case(b"LOGIN")),
|
||||
))(input)
|
||||
}
|
||||
|
||||
fn is_not_tab_or_esc_or_lf(c: u8) -> bool {
|
||||
c != 0x09 && c != 0x01 && c != 0x0a // TAB or 0x01 or LF
|
||||
}
|
||||
|
||||
fn is_esc<'a>(input: &'a [u8]) -> IResult<&'a [u8], &[u8]> {
|
||||
preceded(tag(&[0x01]), take(1usize))(input)
|
||||
}
|
||||
|
||||
fn parameter<'a>(input: &'a [u8]) -> IResult<&'a [u8], &[u8]> {
|
||||
recognize(many1(alt((take_while1(is_not_tab_or_esc_or_lf), is_esc))))(input)
|
||||
}
|
||||
|
||||
fn parameter_str(input: &[u8]) -> IResult<&[u8], String> {
|
||||
let (input, buf) = parameter(input)?;
|
||||
|
||||
std::str::from_utf8(buf)
|
||||
.map(|v| (input, v.to_string()))
|
||||
.map_err(|_| nom::Err::Failure(Error::new(input, ErrorKind::TakeWhile1)))
|
||||
}
|
||||
|
||||
fn is_param_name_char(c: u8) -> bool {
|
||||
is_not_tab_or_esc_or_lf(c) && c != 0x3d // =
|
||||
}
|
||||
|
||||
fn parameter_name(input: &[u8]) -> IResult<&[u8], String> {
|
||||
let (input, buf) = take_while1(is_param_name_char)(input)?;
|
||||
|
||||
std::str::from_utf8(buf)
|
||||
.map(|v| (input, v.to_string()))
|
||||
.map_err(|_| nom::Err::Failure(Error::new(input, ErrorKind::TakeWhile1)))
|
||||
}
|
||||
|
||||
fn service<'a>(input: &'a [u8]) -> IResult<&'a [u8], String> {
|
||||
preceded(tag_no_case("service="), parameter_str)(input)
|
||||
}
|
||||
|
||||
fn auth_option<'a>(input: &'a [u8]) -> IResult<&'a [u8], AuthOption> {
|
||||
use AuthOption::*;
|
||||
alt((
|
||||
alt((
|
||||
value(Debug, tag_no_case(b"debug")),
|
||||
value(NoPenalty, tag_no_case(b"no-penalty")),
|
||||
value(ClientId, tag_no_case(b"client_id")),
|
||||
value(NoLogin, tag_no_case(b"nologin")),
|
||||
map(preceded(tag_no_case(b"session="), u64), |id| Session(id)),
|
||||
map(preceded(tag_no_case(b"lip="), parameter_str), |ip| {
|
||||
LocalIp(ip)
|
||||
}),
|
||||
map(preceded(tag_no_case(b"rip="), parameter_str), |ip| {
|
||||
RemoteIp(ip)
|
||||
}),
|
||||
map(preceded(tag_no_case(b"lport="), u16), |port| {
|
||||
LocalPort(port)
|
||||
}),
|
||||
map(preceded(tag_no_case(b"rport="), u16), |port| {
|
||||
RemotePort(port)
|
||||
}),
|
||||
map(preceded(tag_no_case(b"real_rip="), parameter_str), |ip| {
|
||||
RealRemoteIp(ip)
|
||||
}),
|
||||
map(preceded(tag_no_case(b"real_lip="), parameter_str), |ip| {
|
||||
RealLocalIp(ip)
|
||||
}),
|
||||
map(preceded(tag_no_case(b"real_lport="), u16), |port| {
|
||||
RealLocalPort(port)
|
||||
}),
|
||||
map(preceded(tag_no_case(b"real_rport="), u16), |port| {
|
||||
RealRemotePort(port)
|
||||
}),
|
||||
)),
|
||||
alt((
|
||||
map(
|
||||
preceded(tag_no_case(b"local_name="), parameter_str),
|
||||
|name| LocalName(name),
|
||||
),
|
||||
map(
|
||||
preceded(tag_no_case(b"forward_views="), parameter),
|
||||
|views| ForwardViews(views.into()),
|
||||
),
|
||||
map(preceded(tag_no_case(b"secured="), parameter_str), |info| {
|
||||
Secured(Some(info))
|
||||
}),
|
||||
value(Secured(None), tag_no_case(b"secured")),
|
||||
value(CertUsername, tag_no_case(b"cert_username")),
|
||||
map(preceded(tag_no_case(b"transport="), parameter_str), |ts| {
|
||||
Transport(ts)
|
||||
}),
|
||||
map(
|
||||
preceded(tag_no_case(b"tls_cipher="), parameter_str),
|
||||
|cipher| TlsCipher(cipher),
|
||||
),
|
||||
map(
|
||||
preceded(tag_no_case(b"tls_cipher_bits="), parameter_str),
|
||||
|bits| TlsCipherBits(bits),
|
||||
),
|
||||
map(preceded(tag_no_case(b"tls_pfs="), parameter_str), |pfs| {
|
||||
TlsPfs(pfs)
|
||||
}),
|
||||
map(
|
||||
preceded(tag_no_case(b"tls_protocol="), parameter_str),
|
||||
|proto| TlsProtocol(proto),
|
||||
),
|
||||
map(
|
||||
preceded(tag_no_case(b"valid-client-cert="), parameter_str),
|
||||
|cert| ValidClientCert(cert),
|
||||
),
|
||||
)),
|
||||
alt((
|
||||
map(preceded(tag_no_case(b"resp="), base64), |data| Resp(data)),
|
||||
map(
|
||||
tuple((parameter_name, tag(b"="), parameter)),
|
||||
|(n, _, v)| UnknownPair(n, v.into()),
|
||||
),
|
||||
map(parameter, |v| UnknownBool(v.into())),
|
||||
)),
|
||||
))(input)
|
||||
}
|
||||
|
||||
fn auth_command<'a>(input: &'a [u8]) -> IResult<&'a [u8], ClientCommand> {
|
||||
let mut parser = tuple((
|
||||
tag_no_case(b"AUTH"),
|
||||
tab,
|
||||
u64,
|
||||
tab,
|
||||
mechanism,
|
||||
tab,
|
||||
service,
|
||||
map(opt(preceded(tab, separated_list0(tab, auth_option))), |o| {
|
||||
o.unwrap_or(vec![])
|
||||
}),
|
||||
));
|
||||
let (input, (_, _, id, _, mech, _, service, options)) = parser(input)?;
|
||||
Ok((
|
||||
input,
|
||||
ClientCommand::Auth {
|
||||
id,
|
||||
mech,
|
||||
service,
|
||||
options,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
fn is_base64_core(c: u8) -> bool {
|
||||
c >= 0x30 && c <= 0x39 // 0-9
|
||||
|| c >= 0x41 && c <= 0x5a // A-Z
|
||||
|| c >= 0x61 && c <= 0x7a // a-z
|
||||
|| c == 0x2b // +
|
||||
|| c == 0x2f // /
|
||||
}
|
||||
|
||||
fn is_base64_pad(c: u8) -> bool {
|
||||
c == 0x3d // =
|
||||
}
|
||||
|
||||
fn base64(input: &[u8]) -> IResult<&[u8], Vec<u8>> {
|
||||
let (input, (b64, _)) = tuple((take_while1(is_base64_core), take_while(is_base64_pad)))(input)?;
|
||||
|
||||
let data = base64::engine::general_purpose::STANDARD_NO_PAD
|
||||
.decode(b64)
|
||||
.map_err(|_| nom::Err::Failure(Error::new(input, ErrorKind::TakeWhile1)))?;
|
||||
|
||||
Ok((input, data))
|
||||
}
|
||||
|
||||
/// @FIXME Dovecot does not say if base64 content must be padded or not
|
||||
fn cont_command<'a>(input: &'a [u8]) -> IResult<&'a [u8], ClientCommand> {
|
||||
let mut parser = tuple((tag_no_case(b"CONT"), tab, u64, tab, base64));
|
||||
|
||||
let (input, (_, _, id, _, data)) = parser(input)?;
|
||||
Ok((input, ClientCommand::Cont { id, data }))
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
//
|
||||
// SASL DECODING
|
||||
//
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
fn not_null(c: u8) -> bool {
|
||||
c != 0x0
|
||||
}
|
||||
|
||||
// impersonated user, login, password
|
||||
pub fn auth_plain<'a>(input: &'a [u8]) -> IResult<&'a [u8], (&'a [u8], &'a [u8], &'a [u8])> {
|
||||
map(
|
||||
tuple((
|
||||
take_while(not_null),
|
||||
take(1usize),
|
||||
take_while(not_null),
|
||||
take(1usize),
|
||||
rest,
|
||||
)),
|
||||
|(imp, _, user, _, pass)| (imp, user, pass),
|
||||
)(input)
|
||||
}
|
|
@ -0,0 +1,157 @@
|
|||
use anyhow::Result;
|
||||
use base64::Engine;
|
||||
use tokio_util::bytes::{BufMut, BytesMut};
|
||||
|
||||
use super::types::*;
|
||||
|
||||
pub trait Encode {
|
||||
fn encode(&self, out: &mut BytesMut) -> Result<()>;
|
||||
}
|
||||
|
||||
fn tab_enc(out: &mut BytesMut) {
|
||||
out.put(&[0x09][..])
|
||||
}
|
||||
|
||||
fn lf_enc(out: &mut BytesMut) {
|
||||
out.put(&[0x0A][..])
|
||||
}
|
||||
|
||||
impl Encode for Mechanism {
|
||||
fn encode(&self, out: &mut BytesMut) -> Result<()> {
|
||||
match self {
|
||||
Self::Plain => out.put(&b"PLAIN"[..]),
|
||||
Self::Login => out.put(&b"LOGIN"[..]),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Encode for MechanismParameters {
|
||||
fn encode(&self, out: &mut BytesMut) -> Result<()> {
|
||||
match self {
|
||||
Self::Anonymous => out.put(&b"anonymous"[..]),
|
||||
Self::PlainText => out.put(&b"plaintext"[..]),
|
||||
Self::Dictionary => out.put(&b"dictionary"[..]),
|
||||
Self::Active => out.put(&b"active"[..]),
|
||||
Self::ForwardSecrecy => out.put(&b"forward-secrecy"[..]),
|
||||
Self::MutualAuth => out.put(&b"mutual-auth"[..]),
|
||||
Self::Private => out.put(&b"private"[..]),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Encode for FailCode {
|
||||
fn encode(&self, out: &mut BytesMut) -> Result<()> {
|
||||
match self {
|
||||
Self::TempFail => out.put(&b"temp_fail"[..]),
|
||||
Self::AuthzFail => out.put(&b"authz_fail"[..]),
|
||||
Self::UserDisabled => out.put(&b"user_disabled"[..]),
|
||||
Self::PassExpired => out.put(&b"pass_expired"[..]),
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Encode for ServerCommand {
|
||||
fn encode(&self, out: &mut BytesMut) -> Result<()> {
|
||||
match self {
|
||||
Self::Version(Version { major, minor }) => {
|
||||
out.put(&b"VERSION"[..]);
|
||||
tab_enc(out);
|
||||
out.put(major.to_string().as_bytes());
|
||||
tab_enc(out);
|
||||
out.put(minor.to_string().as_bytes());
|
||||
lf_enc(out);
|
||||
}
|
||||
Self::Spid(pid) => {
|
||||
out.put(&b"SPID"[..]);
|
||||
tab_enc(out);
|
||||
out.put(pid.to_string().as_bytes());
|
||||
lf_enc(out);
|
||||
}
|
||||
Self::Cuid(pid) => {
|
||||
out.put(&b"CUID"[..]);
|
||||
tab_enc(out);
|
||||
out.put(pid.to_string().as_bytes());
|
||||
lf_enc(out);
|
||||
}
|
||||
Self::Cookie(cval) => {
|
||||
out.put(&b"COOKIE"[..]);
|
||||
tab_enc(out);
|
||||
out.put(hex::encode(cval).as_bytes());
|
||||
lf_enc(out);
|
||||
}
|
||||
Self::Mech { kind, parameters } => {
|
||||
out.put(&b"MECH"[..]);
|
||||
tab_enc(out);
|
||||
kind.encode(out)?;
|
||||
for p in parameters.iter() {
|
||||
tab_enc(out);
|
||||
p.encode(out)?;
|
||||
}
|
||||
lf_enc(out);
|
||||
}
|
||||
Self::Done => {
|
||||
out.put(&b"DONE"[..]);
|
||||
lf_enc(out);
|
||||
}
|
||||
Self::Cont { id, data } => {
|
||||
out.put(&b"CONT"[..]);
|
||||
tab_enc(out);
|
||||
out.put(id.to_string().as_bytes());
|
||||
tab_enc(out);
|
||||
if let Some(rdata) = data {
|
||||
let b64 = base64::engine::general_purpose::STANDARD.encode(rdata);
|
||||
out.put(b64.as_bytes());
|
||||
}
|
||||
lf_enc(out);
|
||||
}
|
||||
Self::Ok {
|
||||
id,
|
||||
user_id,
|
||||
extra_parameters,
|
||||
} => {
|
||||
out.put(&b"OK"[..]);
|
||||
tab_enc(out);
|
||||
out.put(id.to_string().as_bytes());
|
||||
if let Some(user) = user_id {
|
||||
tab_enc(out);
|
||||
out.put(&b"user="[..]);
|
||||
out.put(user.as_bytes());
|
||||
}
|
||||
for p in extra_parameters.iter() {
|
||||
tab_enc(out);
|
||||
out.put(&p[..]);
|
||||
}
|
||||
lf_enc(out);
|
||||
}
|
||||
Self::Fail {
|
||||
id,
|
||||
user_id,
|
||||
code,
|
||||
extra_parameters,
|
||||
} => {
|
||||
out.put(&b"FAIL"[..]);
|
||||
tab_enc(out);
|
||||
out.put(id.to_string().as_bytes());
|
||||
if let Some(user) = user_id {
|
||||
tab_enc(out);
|
||||
out.put(&b"user="[..]);
|
||||
out.put(user.as_bytes());
|
||||
}
|
||||
if let Some(code_val) = code {
|
||||
tab_enc(out);
|
||||
out.put(&b"code="[..]);
|
||||
code_val.encode(out)?;
|
||||
}
|
||||
for p in extra_parameters.iter() {
|
||||
tab_enc(out);
|
||||
out.put(&p[..]);
|
||||
}
|
||||
lf_enc(out);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,201 @@
|
|||
use futures::Future;
|
||||
use rand::prelude::*;
|
||||
|
||||
use super::types::*;
|
||||
use super::decode::auth_plain;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum AuthRes {
|
||||
Success(String),
|
||||
Failed(Option<String>, Option<FailCode>),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum State {
|
||||
Error,
|
||||
Init,
|
||||
HandshakePart(Version),
|
||||
HandshakeDone,
|
||||
AuthPlainProgress { id: u64 },
|
||||
AuthDone { id: u64, res: AuthRes },
|
||||
}
|
||||
|
||||
const SERVER_MAJOR: u64 = 1;
|
||||
const SERVER_MINOR: u64 = 2;
|
||||
const EMPTY_AUTHZ: &[u8] = &[];
|
||||
impl State {
|
||||
pub fn new() -> Self {
|
||||
Self::Init
|
||||
}
|
||||
|
||||
async fn try_auth_plain<X, F>(&self, data: &[u8], login: X) -> AuthRes
|
||||
where
|
||||
X: FnOnce(String, String) -> F,
|
||||
F: Future<Output=bool>,
|
||||
{
|
||||
// Check that we can extract user's login+pass
|
||||
let (ubin, pbin) = match auth_plain(&data) {
|
||||
Ok(([], (authz, user, pass))) if authz == user || authz == EMPTY_AUTHZ => (user, pass),
|
||||
Ok(_) => {
|
||||
tracing::error!("Impersonating user is not supported");
|
||||
return AuthRes::Failed(None, None);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(err=?e, "Could not parse the SASL PLAIN data chunk");
|
||||
return AuthRes::Failed(None, None);
|
||||
}
|
||||
};
|
||||
|
||||
// Try to convert it to UTF-8
|
||||
let (user, password) = match (std::str::from_utf8(ubin), std::str::from_utf8(pbin)) {
|
||||
(Ok(u), Ok(p)) => (u, p),
|
||||
_ => {
|
||||
tracing::error!("Username or password contain invalid UTF-8 characters");
|
||||
return AuthRes::Failed(None, None);
|
||||
}
|
||||
};
|
||||
|
||||
// Try to connect user
|
||||
match login(user.to_string(), password.to_string()).await {
|
||||
true => AuthRes::Success(user.to_string()),
|
||||
false => {
|
||||
tracing::warn!("login failed");
|
||||
AuthRes::Failed(Some(user.to_string()), None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn progress<F,X>(&mut self, cmd: ClientCommand, login: X)
|
||||
where
|
||||
X: FnOnce(String, String) -> F,
|
||||
F: Future<Output=bool>,
|
||||
{
|
||||
let new_state = 'state: {
|
||||
match (std::mem::replace(self, State::Error), cmd) {
|
||||
(Self::Init, ClientCommand::Version(v)) => Self::HandshakePart(v),
|
||||
(Self::HandshakePart(version), ClientCommand::Cpid(_cpid)) => {
|
||||
if version.major != SERVER_MAJOR {
|
||||
tracing::error!(
|
||||
client_major = version.major,
|
||||
server_major = SERVER_MAJOR,
|
||||
"Unsupported client major version"
|
||||
);
|
||||
break 'state Self::Error;
|
||||
}
|
||||
|
||||
Self::HandshakeDone
|
||||
}
|
||||
(
|
||||
Self::HandshakeDone { .. },
|
||||
ClientCommand::Auth {
|
||||
id, mech, options, ..
|
||||
},
|
||||
)
|
||||
| (
|
||||
Self::AuthDone { .. },
|
||||
ClientCommand::Auth {
|
||||
id, mech, options, ..
|
||||
},
|
||||
) => {
|
||||
if mech != Mechanism::Plain {
|
||||
tracing::error!(mechanism=?mech, "Unsupported Authentication Mechanism");
|
||||
break 'state Self::AuthDone {
|
||||
id,
|
||||
res: AuthRes::Failed(None, None),
|
||||
};
|
||||
}
|
||||
|
||||
match options.last() {
|
||||
Some(AuthOption::Resp(data)) => Self::AuthDone {
|
||||
id,
|
||||
res: self.try_auth_plain(&data, login).await,
|
||||
},
|
||||
_ => Self::AuthPlainProgress { id },
|
||||
}
|
||||
}
|
||||
(Self::AuthPlainProgress { id }, ClientCommand::Cont { id: cid, data }) => {
|
||||
// Check that ID matches
|
||||
if cid != id {
|
||||
tracing::error!(
|
||||
auth_id = id,
|
||||
cont_id = cid,
|
||||
"CONT id does not match AUTH id"
|
||||
);
|
||||
break 'state Self::AuthDone {
|
||||
id,
|
||||
res: AuthRes::Failed(None, None),
|
||||
};
|
||||
}
|
||||
|
||||
Self::AuthDone {
|
||||
id,
|
||||
res: self.try_auth_plain(&data, login).await,
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
tracing::error!("This command is not valid in this context");
|
||||
Self::Error
|
||||
}
|
||||
}
|
||||
};
|
||||
tracing::debug!(state=?new_state, "Made progress");
|
||||
*self = new_state;
|
||||
}
|
||||
|
||||
pub fn response(&self) -> Vec<ServerCommand> {
|
||||
let mut srv_cmd: Vec<ServerCommand> = Vec::new();
|
||||
|
||||
match self {
|
||||
Self::HandshakeDone { .. } => {
|
||||
srv_cmd.push(ServerCommand::Version(Version {
|
||||
major: SERVER_MAJOR,
|
||||
minor: SERVER_MINOR,
|
||||
}));
|
||||
|
||||
srv_cmd.push(ServerCommand::Mech {
|
||||
kind: Mechanism::Plain,
|
||||
parameters: vec![MechanismParameters::PlainText],
|
||||
});
|
||||
|
||||
srv_cmd.push(ServerCommand::Spid(15u64));
|
||||
srv_cmd.push(ServerCommand::Cuid(19350u64));
|
||||
|
||||
let mut cookie = [0u8; 16];
|
||||
thread_rng().fill(&mut cookie);
|
||||
srv_cmd.push(ServerCommand::Cookie(cookie));
|
||||
|
||||
srv_cmd.push(ServerCommand::Done);
|
||||
}
|
||||
Self::AuthPlainProgress { id } => {
|
||||
srv_cmd.push(ServerCommand::Cont {
|
||||
id: *id,
|
||||
data: None,
|
||||
});
|
||||
}
|
||||
Self::AuthDone {
|
||||
id,
|
||||
res: AuthRes::Success(user),
|
||||
} => {
|
||||
srv_cmd.push(ServerCommand::Ok {
|
||||
id: *id,
|
||||
user_id: Some(user.to_string()),
|
||||
extra_parameters: vec![],
|
||||
});
|
||||
}
|
||||
Self::AuthDone {
|
||||
id,
|
||||
res: AuthRes::Failed(maybe_user, maybe_failcode),
|
||||
} => {
|
||||
srv_cmd.push(ServerCommand::Fail {
|
||||
id: *id,
|
||||
user_id: maybe_user.clone(),
|
||||
code: maybe_failcode.clone(),
|
||||
extra_parameters: vec![],
|
||||
});
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
|
||||
srv_cmd
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
/// Seek compatibility with the Dovecot Authentication Protocol
|
||||
///
|
||||
/// ## Trace
|
||||
///
|
||||
/// ```text
|
||||
/// S: VERSION 1 2
|
||||
/// S: MECH PLAIN plaintext
|
||||
/// S: MECH LOGIN plaintext
|
||||
/// S: SPID 15
|
||||
/// S: CUID 17654
|
||||
/// S: COOKIE f56692bee41f471ed01bd83520025305
|
||||
/// S: DONE
|
||||
/// C: VERSION 1 2
|
||||
/// C: CPID 1
|
||||
///
|
||||
/// C: AUTH 2 PLAIN service=smtp
|
||||
/// S: CONT 2
|
||||
/// C: CONT 2 base64stringFollowingRFC4616==
|
||||
/// S: OK 2 user=alice@example.tld
|
||||
///
|
||||
/// C: AUTH 42 LOGIN service=smtp
|
||||
/// S: CONT 42 VXNlcm5hbWU6
|
||||
/// C: CONT 42 b64User
|
||||
/// S: CONT 42 UGFzc3dvcmQ6
|
||||
/// C: CONT 42 b64Pass
|
||||
/// S: FAIL 42 user=alice
|
||||
/// ```
|
||||
///
|
||||
/// ## RFC References
|
||||
///
|
||||
/// PLAIN SASL - https://datatracker.ietf.org/doc/html/rfc4616
|
||||
///
|
||||
///
|
||||
/// ## Dovecot References
|
||||
///
|
||||
/// https://doc.dovecot.org/developer_manual/design/auth_protocol/
|
||||
/// https://doc.dovecot.org/configuration_manual/authentication/authentication_mechanisms/#authentication-authentication-mechanisms
|
||||
/// https://doc.dovecot.org/configuration_manual/howto/simple_virtual_install/#simple-virtual-install-smtp-auth
|
||||
/// https://doc.dovecot.org/configuration_manual/howto/postfix_and_dovecot_sasl/#howto-postfix-and-dovecot-sasl
|
||||
pub mod types;
|
||||
pub mod encode;
|
||||
pub mod decode;
|
||||
pub mod flow;
|
|
@ -0,0 +1,163 @@
|
|||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Mechanism {
|
||||
Plain,
|
||||
Login,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum AuthOption {
|
||||
/// Unique session ID. Mainly used for logging.
|
||||
Session(u64),
|
||||
/// Local IP connected to by the client. In standard string format, e.g. 127.0.0.1 or ::1.
|
||||
LocalIp(String),
|
||||
/// Remote client IP
|
||||
RemoteIp(String),
|
||||
/// Local port connected to by the client.
|
||||
LocalPort(u16),
|
||||
/// Remote client port
|
||||
RemotePort(u16),
|
||||
/// When Dovecot proxy is used, the real_rip/real_port are the proxy’s IP/port and real_lip/real_lport are the backend’s IP/port where the proxy was connected to.
|
||||
RealRemoteIp(String),
|
||||
RealLocalIp(String),
|
||||
RealLocalPort(u16),
|
||||
RealRemotePort(u16),
|
||||
/// TLS SNI name
|
||||
LocalName(String),
|
||||
/// Enable debugging for this lookup.
|
||||
Debug,
|
||||
/// List of fields that will become available via %{forward_*} variables. The list is double-tab-escaped, like: tab_escaped[tab_escaped(key=value)[<TAB>...]
|
||||
/// Note: we do not unescape the tabulation, and thus we don't parse the data
|
||||
ForwardViews(Vec<u8>),
|
||||
/// Remote user has secured transport to auth client (e.g. localhost, SSL, TLS).
|
||||
Secured(Option<String>),
|
||||
/// The value can be “insecure”, “trusted” or “TLS”.
|
||||
Transport(String),
|
||||
/// TLS cipher being used.
|
||||
TlsCipher(String),
|
||||
/// The number of bits in the TLS cipher.
|
||||
/// @FIXME: I don't know how if it's a string or an integer
|
||||
TlsCipherBits(String),
|
||||
/// TLS perfect forward secrecy algorithm (e.g. DH, ECDH)
|
||||
TlsPfs(String),
|
||||
/// TLS protocol name (e.g. SSLv3, TLSv1.2)
|
||||
TlsProtocol(String),
|
||||
/// Remote user has presented a valid SSL certificate.
|
||||
ValidClientCert(String),
|
||||
/// Ignore auth penalty tracking for this request
|
||||
NoPenalty,
|
||||
/// Unknown option sent by Postfix
|
||||
NoLogin,
|
||||
/// Username taken from client’s SSL certificate.
|
||||
CertUsername,
|
||||
/// IMAP ID string
|
||||
ClientId,
|
||||
/// An unknown key
|
||||
UnknownPair(String, Vec<u8>),
|
||||
UnknownBool(Vec<u8>),
|
||||
/// Initial response for authentication mechanism.
|
||||
/// NOTE: This must be the last parameter. Everything after it is ignored.
|
||||
/// This is to avoid accidental security holes if user-given data is directly put to base64 string without filtering out tabs.
|
||||
/// **This field is used when the data to pass is small, it's a way to "inline a continuation".
|
||||
Resp(Vec<u8>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Version {
|
||||
pub major: u64,
|
||||
pub minor: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ClientCommand {
|
||||
/// Both client and server should check that they support the same major version number. If they don’t, the other side isn’t expected to be talking the same protocol and should be disconnected. Minor version can be ignored. This document specifies the version number 1.2.
|
||||
Version(Version),
|
||||
/// CPID finishes the handshake from client.
|
||||
Cpid(u64),
|
||||
Auth {
|
||||
/// ID is a connection-specific unique request identifier. It must be a 32bit number, so typically you’d just increment it by one.
|
||||
id: u64,
|
||||
/// A SASL mechanism (eg. LOGIN, PLAIN, etc.)
|
||||
/// See: https://doc.dovecot.org/configuration_manual/authentication/authentication_mechanisms/#authentication-authentication-mechanisms
|
||||
mech: Mechanism,
|
||||
/// Service is the service requesting authentication, eg. pop3, imap, smtp.
|
||||
service: String,
|
||||
/// All the optional parameters
|
||||
options: Vec<AuthOption>,
|
||||
},
|
||||
Cont {
|
||||
/// The <id> must match the <id> of the AUTH command.
|
||||
id: u64,
|
||||
/// Data that will be serialized to / deserialized from base64
|
||||
data: Vec<u8>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum MechanismParameters {
|
||||
/// Anonymous authentication
|
||||
Anonymous,
|
||||
/// Transfers plaintext passwords
|
||||
PlainText,
|
||||
/// Subject to passive (dictionary) attack
|
||||
Dictionary,
|
||||
/// Subject to active (non-dictionary) attack
|
||||
Active,
|
||||
/// Provides forward secrecy between sessions
|
||||
ForwardSecrecy,
|
||||
/// Provides mutual authentication
|
||||
MutualAuth,
|
||||
/// Don’t advertise this as available SASL mechanism (eg. APOP)
|
||||
Private,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum FailCode {
|
||||
/// This is a temporary internal failure, e.g. connection was lost to SQL database.
|
||||
TempFail,
|
||||
/// Authentication succeeded, but authorization failed (master user’s password was ok, but destination user was not ok).
|
||||
AuthzFail,
|
||||
/// User is disabled (password may or may not have been correct)
|
||||
UserDisabled,
|
||||
/// User’s password has expired.
|
||||
PassExpired,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ServerCommand {
|
||||
/// Both client and server should check that they support the same major version number. If they don’t, the other side isn’t expected to be talking the same protocol and should be disconnected. Minor version can be ignored. This document specifies the version number 1.2.
|
||||
Version(Version),
|
||||
/// CPID and SPID specify client and server Process Identifiers (PIDs). They should be unique identifiers for the specific process. UNIX process IDs are good choices.
|
||||
/// SPID can be used by authentication client to tell master which server process handled the authentication.
|
||||
Spid(u64),
|
||||
/// CUID is a server process-specific unique connection identifier. It’s different each time a connection is established for the server.
|
||||
/// CUID is currently useful only for APOP authentication.
|
||||
Cuid(u64),
|
||||
Mech {
|
||||
kind: Mechanism,
|
||||
parameters: Vec<MechanismParameters>,
|
||||
},
|
||||
/// COOKIE returns connection-specific 128 bit cookie in hex. It must be given to REQUEST command. (Protocol v1.1+ / Dovecot v2.0+)
|
||||
Cookie([u8; 16]),
|
||||
/// DONE finishes the handshake from server.
|
||||
Done,
|
||||
|
||||
Fail {
|
||||
id: u64,
|
||||
user_id: Option<String>,
|
||||
code: Option<FailCode>,
|
||||
extra_parameters: Vec<Vec<u8>>,
|
||||
},
|
||||
Cont {
|
||||
id: u64,
|
||||
data: Option<Vec<u8>>,
|
||||
},
|
||||
/// FAIL and OK may contain multiple unspecified parameters which authentication client may handle specially.
|
||||
/// The only one specified here is user=<userid> parameter, which should always be sent if the userid is known.
|
||||
Ok {
|
||||
id: u64,
|
||||
user_id: Option<String>,
|
||||
extra_parameters: Vec<Vec<u8>>,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
[package]
|
||||
name = "aero-user"
|
||||
version = "0.3.0"
|
||||
authors = ["Alex Auvolat <alex@adnab.me>", "Quentin Dufour <quentin@dufour.io>"]
|
||||
edition = "2021"
|
||||
license = "EUPL-1.2"
|
||||
description = "Represent an encrypted user profile"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
serde.workspace = true
|
||||
zstd.workspace = true
|
||||
sodiumoxide.workspace = true
|
||||
log.workspace = true
|
||||
async-trait.workspace = true
|
||||
ldap3.workspace = true
|
||||
base64.workspace = true
|
||||
rand.workspace = true
|
||||
tokio.workspace = true
|
||||
aws-config.workspace = true
|
||||
aws-sdk-s3.workspace = true
|
||||
aws-smithy-runtime.workspace = true
|
||||
aws-smithy-runtime-api.workspace = true
|
||||
hyper-rustls.workspace = true
|
||||
hyper-util.workspace = true
|
||||
k2v-client.workspace = true
|
||||
rmp-serde.workspace = true
|
||||
toml.workspace = true
|
||||
tracing.workspace = true
|
||||
argon2.workspace = true
|
|
@ -10,6 +10,7 @@ use serde::{Deserialize, Serialize};
|
|||
pub struct CompanionConfig {
|
||||
pub pid: Option<PathBuf>,
|
||||
pub imap: ImapUnsecureConfig,
|
||||
// @FIXME Add DAV
|
||||
|
||||
#[serde(flatten)]
|
||||
pub users: LoginStaticConfig,
|
||||
|
@ -22,6 +23,8 @@ pub struct ProviderConfig {
|
|||
pub imap_unsecure: Option<ImapUnsecureConfig>,
|
||||
pub lmtp: Option<LmtpConfig>,
|
||||
pub auth: Option<AuthConfig>,
|
||||
pub dav: Option<DavConfig>,
|
||||
pub dav_unsecure: Option<DavUnsecureConfig>,
|
||||
pub users: UserManagement,
|
||||
}
|
||||
|
||||
|
@ -51,6 +54,18 @@ pub struct ImapConfig {
|
|||
pub key: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct DavUnsecureConfig {
|
||||
pub bind_addr: SocketAddr,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct DavConfig {
|
||||
pub bind_addr: SocketAddr,
|
||||
pub certs: PathBuf,
|
||||
pub key: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct ImapUnsecureConfig {
|
||||
pub bind_addr: SocketAddr,
|
|
@ -0,0 +1,9 @@
|
|||
pub mod config;
|
||||
pub mod cryptoblob;
|
||||
pub mod login;
|
||||
pub mod storage;
|
||||
|
||||
// A user is composed of 3 things:
|
||||
// - An identity (login)
|
||||
// - A storage profile (storage)
|
||||
// - Some cryptography data (cryptoblob)
|
|
@ -1,11 +1,10 @@
|
|||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use ldap3::{LdapConnAsync, Scope, SearchEntry};
|
||||
use log::debug;
|
||||
|
||||
use crate::config::*;
|
||||
use crate::login::*;
|
||||
use crate::storage;
|
||||
use super::*;
|
||||
|
||||
pub struct LdapLoginProvider {
|
||||
ldap_server: String,
|
|
@ -2,11 +2,11 @@ pub mod demo_provider;
|
|||
pub mod ldap_provider;
|
||||
pub mod static_provider;
|
||||
|
||||
use base64::Engine;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use async_trait::async_trait;
|
||||
use base64::Engine;
|
||||
use rand::prelude::*;
|
||||
|
||||
use crate::cryptoblob::*;
|
|
@ -1,12 +1,11 @@
|
|||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, bail};
|
||||
use async_trait::async_trait;
|
||||
use tokio::signal::unix::{signal, SignalKind};
|
||||
use tokio::sync::watch;
|
||||
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::config::*;
|
||||
use crate::login::*;
|
||||
use crate::storage;
|
|
@ -6,7 +6,7 @@ use hyper_util::client::legacy::{connect::HttpConnector, Client as HttpClient};
|
|||
use hyper_util::rt::TokioExecutor;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::storage::*;
|
||||
use super::*;
|
||||
|
||||
pub struct GarageRoot {
|
||||
k2v_http: HttpClient<HttpsConnector<HttpConnector>, k2v_client::Body>,
|
||||
|
@ -426,15 +426,16 @@ impl IStore for GarageStore {
|
|||
tracing::debug!("Fetched {}/{}", self.bucket, blob_ref.0);
|
||||
Ok(bv)
|
||||
}
|
||||
async fn blob_insert(&self, blob_val: BlobVal) -> Result<(), StorageError> {
|
||||
async fn blob_insert(&self, blob_val: BlobVal) -> Result<String, StorageError> {
|
||||
tracing::trace!(entry=%blob_val.blob_ref, command="blob_insert");
|
||||
let streamable_value = s3::primitives::ByteStream::from(blob_val.value);
|
||||
let obj_key = blob_val.blob_ref.0;
|
||||
|
||||
let maybe_send = self
|
||||
.s3
|
||||
.put_object()
|
||||
.bucket(self.bucket.to_string())
|
||||
.key(blob_val.blob_ref.0.to_string())
|
||||
.key(obj_key.to_string())
|
||||
.set_metadata(Some(blob_val.meta))
|
||||
.body(streamable_value)
|
||||
.send()
|
||||
|
@ -445,9 +446,12 @@ impl IStore for GarageStore {
|
|||
tracing::error!("unable to send object: {}", e);
|
||||
Err(StorageError::Internal)
|
||||
}
|
||||
Ok(_) => {
|
||||
tracing::debug!("Inserted {}/{}", self.bucket, blob_val.blob_ref.0);
|
||||
Ok(())
|
||||
Ok(put_output) => {
|
||||
tracing::debug!("Inserted {}/{}", self.bucket, obj_key);
|
||||
Ok(put_output
|
||||
.e_tag()
|
||||
.map(|v| format!("\"{}\"", v))
|
||||
.unwrap_or(format!("W/\"{}\"", obj_key)))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,9 +1,12 @@
|
|||
use crate::storage::*;
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::collections::BTreeMap;
|
||||
use std::ops::Bound::{self, Excluded, Included, Unbounded};
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::sync::RwLock;
|
||||
|
||||
use sodiumoxide::{hex, crypto::hash};
|
||||
use tokio::sync::Notify;
|
||||
|
||||
use crate::storage::*;
|
||||
|
||||
/// This implementation is very inneficient, and not completely correct
|
||||
/// Indeed, when the connector is dropped, the memory is freed.
|
||||
/// It means that when a user disconnects, its data are lost.
|
||||
|
@ -80,6 +83,12 @@ impl InternalBlobVal {
|
|||
value: self.data.clone(),
|
||||
}
|
||||
}
|
||||
fn etag(&self) -> String {
|
||||
let digest = hash::hash(self.data.as_ref());
|
||||
let buff = digest.as_ref();
|
||||
let hexstr = hex::encode(buff);
|
||||
format!("\"{}\"", hexstr)
|
||||
}
|
||||
}
|
||||
|
||||
type ArcRow = Arc<RwLock<HashMap<String, BTreeMap<String, InternalRowVal>>>>;
|
||||
|
@ -300,13 +309,14 @@ impl IStore for MemStore {
|
|||
.ok_or(StorageError::NotFound)
|
||||
.map(|v| v.to_blob_val(blob_ref))
|
||||
}
|
||||
async fn blob_insert(&self, blob_val: BlobVal) -> Result<(), StorageError> {
|
||||
async fn blob_insert(&self, blob_val: BlobVal) -> Result<String, StorageError> {
|
||||
tracing::trace!(entry=%blob_val.blob_ref, command="blob_insert");
|
||||
let mut store = self.blob.write().or(Err(StorageError::Internal))?;
|
||||
let entry = store.entry(blob_val.blob_ref.0.clone()).or_default();
|
||||
entry.data = blob_val.value.clone();
|
||||
entry.metadata = blob_val.meta.clone();
|
||||
Ok(())
|
||||
|
||||
Ok(entry.etag())
|
||||
}
|
||||
async fn blob_copy(&self, src: &BlobRef, dst: &BlobRef) -> Result<(), StorageError> {
|
||||
tracing::trace!(src=%src, dst=%dst, command="blob_copy");
|
|
@ -11,11 +11,12 @@
|
|||
pub mod garage;
|
||||
pub mod in_memory;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use std::collections::HashMap;
|
||||
use std::hash::Hash;
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Alternative {
|
||||
Tombstone,
|
||||
|
@ -158,7 +159,7 @@ pub trait IStore {
|
|||
async fn row_poll(&self, value: &RowRef) -> Result<RowVal, StorageError>;
|
||||
|
||||
async fn blob_fetch(&self, blob_ref: &BlobRef) -> Result<BlobVal, StorageError>;
|
||||
async fn blob_insert(&self, blob_val: BlobVal) -> Result<(), StorageError>;
|
||||
async fn blob_insert(&self, blob_val: BlobVal) -> Result<String, StorageError>;
|
||||
async fn blob_copy(&self, src: &BlobRef, dst: &BlobRef) -> Result<(), StorageError>;
|
||||
async fn blob_list(&self, prefix: &str) -> Result<Vec<BlobRef>, StorageError>;
|
||||
async fn blob_rm(&self, blob_ref: &BlobRef) -> Result<(), StorageError>;
|
|
@ -0,0 +1,27 @@
|
|||
[package]
|
||||
name = "aerogramme"
|
||||
version = "0.3.0"
|
||||
authors = ["Alex Auvolat <alex@adnab.me>", "Quentin Dufour <quentin@dufour.io>"]
|
||||
edition = "2021"
|
||||
license = "EUPL-1.2"
|
||||
description = "A robust email server"
|
||||
|
||||
[dependencies]
|
||||
aero-user.workspace = true
|
||||
aero-proto.workspace = true
|
||||
|
||||
anyhow.workspace = true
|
||||
backtrace.workspace = true
|
||||
futures.workspace = true
|
||||
tokio.workspace = true
|
||||
log.workspace = true
|
||||
nix.workspace = true
|
||||
clap.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
rpassword.workspace = true
|
||||
|
||||
[[test]]
|
||||
name = "behavior"
|
||||
path = "tests/behavior.rs"
|
||||
harness = false
|
|
@ -1,17 +1,4 @@
|
|||
#![feature(async_fn_in_trait)]
|
||||
|
||||
mod auth;
|
||||
mod bayou;
|
||||
mod config;
|
||||
mod cryptoblob;
|
||||
mod imap;
|
||||
mod k2v_util;
|
||||
mod lmtp;
|
||||
mod login;
|
||||
mod mail;
|
||||
mod server;
|
||||
mod storage;
|
||||
mod timestamp;
|
||||
|
||||
use std::io::Read;
|
||||
use std::path::PathBuf;
|
||||
|
@ -20,9 +7,9 @@ use anyhow::{bail, Context, Result};
|
|||
use clap::{Parser, Subcommand};
|
||||
use nix::{sys::signal, unistd::Pid};
|
||||
|
||||
use config::*;
|
||||
use login::{static_provider::*, *};
|
||||
use server::Server;
|
||||
use aero_user::config::*;
|
||||
use aero_user::login::{static_provider::*, *};
|
||||
use crate::server::Server;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[clap(author, version, about, long_about = None)]
|
||||
|
@ -166,7 +153,7 @@ fn tracer() {
|
|||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
if std::env::var("RUST_LOG").is_err() {
|
||||
std::env::set_var("RUST_LOG", "main=info,aerogramme=info,k2v_client=info")
|
||||
std::env::set_var("RUST_LOG", "info")
|
||||
}
|
||||
|
||||
// Abort on panic (same behavior as in Go)
|
||||
|
@ -184,9 +171,13 @@ async fn main() -> Result<()> {
|
|||
AnyConfig::Provider(ProviderConfig {
|
||||
pid: None,
|
||||
imap: None,
|
||||
dav: None,
|
||||
imap_unsecure: Some(ImapUnsecureConfig {
|
||||
bind_addr: SocketAddr::new(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), 1143),
|
||||
}),
|
||||
dav_unsecure: Some(DavUnsecureConfig {
|
||||
bind_addr: SocketAddr::new(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), 8087),
|
||||
}),
|
||||
lmtp: Some(LmtpConfig {
|
||||
bind_addr: SocketAddr::new(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), 1025),
|
||||
hostname: "example.tld".to_string(),
|
|
@ -7,18 +7,21 @@ use futures::try_join;
|
|||
use log::*;
|
||||
use tokio::sync::watch;
|
||||
|
||||
use crate::auth;
|
||||
use crate::config::*;
|
||||
use crate::imap;
|
||||
use crate::lmtp::*;
|
||||
use crate::login::ArcLoginProvider;
|
||||
use crate::login::{demo_provider::*, ldap_provider::*, static_provider::*};
|
||||
use aero_user::config::*;
|
||||
use aero_user::login::ArcLoginProvider;
|
||||
use aero_user::login::{demo_provider::*, ldap_provider::*, static_provider::*};
|
||||
use aero_proto::sasl as auth;
|
||||
use aero_proto::dav;
|
||||
use aero_proto::imap;
|
||||
use aero_proto::lmtp::*;
|
||||
|
||||
pub struct Server {
|
||||
lmtp_server: Option<Arc<LmtpServer>>,
|
||||
imap_unsecure_server: Option<imap::Server>,
|
||||
imap_server: Option<imap::Server>,
|
||||
auth_server: Option<auth::AuthServer>,
|
||||
dav_unsecure_server: Option<dav::Server>,
|
||||
dav_server: Option<dav::Server>,
|
||||
pid_file: Option<PathBuf>,
|
||||
}
|
||||
|
||||
|
@ -34,6 +37,8 @@ impl Server {
|
|||
imap_unsecure_server,
|
||||
imap_server: None,
|
||||
auth_server: None,
|
||||
dav_unsecure_server: None,
|
||||
dav_server: None,
|
||||
pid_file: config.pid,
|
||||
})
|
||||
}
|
||||
|
@ -57,11 +62,20 @@ impl Server {
|
|||
let auth_server = config
|
||||
.auth
|
||||
.map(|auth| auth::AuthServer::new(auth, login.clone()));
|
||||
let dav_unsecure_server = config
|
||||
.dav_unsecure
|
||||
.map(|dav_config| dav::new_unsecure(dav_config, login.clone()));
|
||||
let dav_server = config
|
||||
.dav
|
||||
.map(|dav_config| dav::new(dav_config, login.clone()))
|
||||
.transpose()?;
|
||||
|
||||
Ok(Self {
|
||||
lmtp_server,
|
||||
imap_unsecure_server,
|
||||
imap_server,
|
||||
dav_unsecure_server,
|
||||
dav_server,
|
||||
auth_server,
|
||||
pid_file: config.pid,
|
||||
})
|
||||
|
@ -112,6 +126,18 @@ impl Server {
|
|||
None => Ok(()),
|
||||
Some(a) => a.run(exit_signal.clone()).await,
|
||||
}
|
||||
},
|
||||
async {
|
||||
match self.dav_unsecure_server {
|
||||
None => Ok(()),
|
||||
Some(s) => s.run(exit_signal.clone()).await,
|
||||
}
|
||||
},
|
||||
async {
|
||||
match self.dav_server {
|
||||
None => Ok(()),
|
||||
Some(s) => s.run(exit_signal.clone()).await,
|
||||
}
|
||||
}
|
||||
)?;
|
||||
|
|
@ -1 +0,0 @@
|
|||
book
|
|
@ -1,9 +0,0 @@
|
|||
[book]
|
||||
authors = ["Quentin Dufour"]
|
||||
language = "en"
|
||||
multilingual = false
|
||||
src = "src"
|
||||
title = "Aerogramme - Encrypted e-mail storage over Garage"
|
||||
|
||||
[output.html]
|
||||
mathjax-support = true
|
|
@ -1,34 +0,0 @@
|
|||
# Summary
|
||||
|
||||
[Introduction](./index.md)
|
||||
|
||||
# Quick start
|
||||
|
||||
- [Installation](./installation.md)
|
||||
- [Setup](./setup.md)
|
||||
- [Validation](./validate.md)
|
||||
|
||||
# Cookbook
|
||||
|
||||
- [Not ready for production]()
|
||||
|
||||
# Reference
|
||||
|
||||
- [Configuration file](./config.md)
|
||||
- [RFC coverage](./rfc.md)
|
||||
|
||||
# Design
|
||||
|
||||
- [Overview](./overview.md)
|
||||
- [Mailboxes](./mailbox.md)
|
||||
- [Mutation Log](./log.md)
|
||||
- [IMAP UID proof](./imap_uid.md)
|
||||
|
||||
# Internals
|
||||
|
||||
- [Persisted data structures](./data_format.md)
|
||||
- [Cryptography & key management](./crypt-key.md)
|
||||
|
||||
# Development
|
||||
|
||||
- [Notes](./notes.md)
|
Binary file not shown.
Before Width: | Height: | Size: 26 KiB |
Binary file not shown.
Before Width: | Height: | Size: 27 KiB |
Binary file not shown.
Before Width: | Height: | Size: 73 KiB |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue