M Cargo.lock => Cargo.lock +97 -20
@@ 89,6 89,17 @@ dependencies = [
]
[[package]]
+name = "atty"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
+dependencies = [
+ "hermit-abi 0.1.19",
+ "libc",
+ "winapi",
+]
+
+[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ 267,7 278,7 @@ dependencies = [
"hyper-rustls",
"libdav",
"log",
- "simple_logger",
+ "simple_logger 4.2.0",
"tokio",
]
@@ 484,6 495,15 @@ checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]]
name = "hermit-abi"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "hermit-abi"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b"
@@ 577,7 597,7 @@ version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b"
dependencies = [
- "hermit-abi",
+ "hermit-abi 0.3.2",
"rustix",
"windows-sys 0.48.0",
]
@@ 592,6 612,15 @@ dependencies = [
]
[[package]]
+name = "itertools"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57"
+dependencies = [
+ "either",
+]
+
+[[package]]
name = "itoa"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ 614,9 643,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
-version = "0.2.147"
+version = "0.2.149"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
+checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b"
[[package]]
name = "libdav"
@@ 654,7 683,7 @@ dependencies = [
"log",
"rand",
"serde",
- "simple_logger",
+ "simple_logger 4.2.0",
"strum",
"tokio",
"toml 0.7.6",
@@ 708,7 737,7 @@ version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
dependencies = [
- "hermit-abi",
+ "hermit-abi 0.3.2",
"libc",
]
@@ 856,13 885,27 @@ dependencies = [
"cc",
"libc",
"once_cell",
- "spin",
- "untrusted",
+ "spin 0.5.2",
+ "untrusted 0.7.1",
"web-sys",
"winapi",
]
[[package]]
+name = "ring"
+version = "0.17.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb0205304757e5d899b9c2e448b867ffd03ae7f988002e47cd24954391394d0b"
+dependencies = [
+ "cc",
+ "getrandom",
+ "libc",
+ "spin 0.9.8",
+ "untrusted 0.9.0",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
name = "roxmltree"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ 892,12 935,12 @@ dependencies = [
[[package]]
name = "rustls"
-version = "0.21.7"
+version = "0.21.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8"
+checksum = "446e14c5cda4f3f30fe71863c34ec70f5ac79d6087097ad0bb433e1be5edf04c"
dependencies = [
"log",
- "ring",
+ "ring 0.17.5",
"rustls-webpki",
"sct",
]
@@ 925,12 968,12 @@ dependencies = [
[[package]]
name = "rustls-webpki"
-version = "0.101.4"
+version = "0.101.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7d93931baf2d282fff8d3a532bbfd7653f734643161b87e3e01e59a04439bf0d"
+checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765"
dependencies = [
- "ring",
- "untrusted",
+ "ring 0.17.5",
+ "untrusted 0.9.0",
]
[[package]]
@@ 960,8 1003,8 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4"
dependencies = [
- "ring",
- "untrusted",
+ "ring 0.16.20",
+ "untrusted 0.7.1",
]
[[package]]
@@ 1038,6 1081,18 @@ dependencies = [
[[package]]
name = "simple_logger"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48047e77b528151aaf841a10a9025f9459da80ba820e425ff7eb005708a76dc7"
+dependencies = [
+ "atty",
+ "colored",
+ "log",
+ "winapi",
+]
+
+[[package]]
+name = "simple_logger"
version = "4.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2230cd5c29b815c9b699fb610b49a5ed65588f3509d9f0108be3a885da629333"
@@ 1089,6 1144,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
[[package]]
+name = "spin"
+version = "0.9.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
+
+[[package]]
name = "strsim"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ 1179,9 1240,9 @@ checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb"
[[package]]
name = "tokio"
-version = "1.32.0"
+version = "1.33.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9"
+checksum = "4f38200e3ef7995e5ef13baec2f432a6da0aa9ac495b2c0e8f3b7eec2c92d653"
dependencies = [
"backtrace",
"bytes",
@@ 1338,6 1399,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
[[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
+[[package]]
name = "utf8parse"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ 1348,7 1415,17 @@ name = "vdirsyncer"
version = "0.0.1"
dependencies = [
"anyhow",
+ "hyper",
+ "hyper-rustls",
+ "itertools 0.11.0",
+ "libdav",
+ "log",
+ "rustls",
+ "rustls-pemfile",
"serde",
+ "sha2",
+ "simple_logger 2.3.0",
+ "tokio",
"toml 0.8.1",
"vstorage",
]
@@ 1368,7 1445,7 @@ dependencies = [
"http",
"hyper",
"hyper-rustls",
- "itertools",
+ "itertools 0.10.5",
"libdav",
"log",
"rand",
M vdirsyncer/Cargo.toml => vdirsyncer/Cargo.toml +10 -0
@@ 12,6 12,16 @@ license = "EUPL-1.2"
[dependencies]
anyhow = "1.0.75"
+hyper = "0.14.27"
+hyper-rustls = "0.24.1"
+itertools = "0.11.0"
+libdav = { version = "0.1.0", path = "../libdav" }
+log = "0.4.20"
+rustls = { version = "0.21.8", features = ["dangerous_configuration"] }
+rustls-pemfile = "1.0.3"
serde = { version = "1.0.188", features = ["derive"] }
+sha2 = "0.10.2"
+simple_logger = { version = "2.3.0", default-features = false, features = ["colors"] }
+tokio = { version = "1.33.0", features = ["sync", "macros", "rt"] }
toml = { version = "0.8.1", features = ["parse"], default-features = false }
vstorage = { version = "0.1.0", path = "../vstorage" }
M vdirsyncer/src/config.rs => vdirsyncer/src/config.rs +376 -42
@@ 10,44 10,169 @@ use std::{
collections::HashMap,
ffi::OsString,
marker::PhantomData,
+ num::ParseIntError,
os::unix::prelude::{OsStrExt, OsStringExt},
path::{Path, PathBuf},
+ process::{Command, Stdio},
+ sync::Arc,
+ time::SystemTime,
};
+use anyhow::{bail, Context};
+use hyper::client::HttpConnector;
+use hyper_rustls::{ConfigBuilderExt, HttpsConnector, HttpsConnectorBuilder};
+use itertools::Itertools;
+use libdav::auth::Password;
+use rustls::{
+ client::{ServerCertVerified, ServerCertVerifier},
+ CertificateError, ClientConfig, RootCertStore,
+};
use serde::Deserialize;
use vstorage::{
- base::{IcsItem, Item, VcardItem},
- filesystem::FilesystemStorage,
+ base::{IcsItem, Item, Storage, VcardItem},
+ caldav::{CalDavDefinition, CalDavStorage},
+ carddav::{CardDavDefinition, CardDavStorage},
+ filesystem::{FilesystemDefinition, FilesystemStorage},
+ sync::declare::{CollectionDescription, DeclaredMapping, StoragePair, StoragePairBuilder},
+ webcal::{WebCalDefinition, WebCalStorage},
+ CollectionId,
+};
+
+use crate::tls::{
+ cert_and_key_from_pemfile, certs_from_pemfile, key_from_pemfile, FingerPrintAndWebPkiVerifier,
+ FingerPrintVerifier,
};
+/// A deserialised configuration file.
#[derive(Deserialize, Debug)]
-pub struct Config {
+pub(crate) struct Config {
general: GeneralSection,
- pair: HashMap<String, PairSection>,
- storage: HashMap<String, StorageSection>,
+ #[serde(rename = "pair")]
+ pairs: HashMap<String, PairSection>,
+ #[serde(rename = "storage")]
+ storages: HashMap<String, StorageSection>,
}
-#[derive(Deserialize, Debug)]
-struct GeneralSection {
- status_path: PathBuf,
+// TODO: the Config instance should be consumed when converting into Storages and Pairs.
+// this would reduce a lot of pointless cloning and copying values.
+impl Config {
+ /// Returns the `status_path`, expanding a leading tilde if present.
+ pub(crate) fn status_path(&self) -> Cow<Path> {
+ expand_tilde(&self.general.status_path)
+ }
+
+ pub(crate) async fn storages(
+ &self,
+ ) -> anyhow::Result<(
+ HashMap<String, Arc<dyn Storage<IcsItem>>>,
+ HashMap<String, Arc<dyn Storage<VcardItem>>>,
+ )> {
+ let mut calendars = HashMap::new();
+ let mut address_books = HashMap::new();
+
+ for (name, source) in self.storages.iter() {
+ match source.storage().await? {
+ EitherStorage::Calendar(c) => {
+ calendars.insert(name.clone(), c);
+ }
+ EitherStorage::AddressBook(a) => {
+ address_books.insert(name.clone(), a);
+ }
+ }
+ }
+
+ Ok((calendars, address_books))
+ }
+
+ pub(crate) fn pairs<'storages>(
+ &self,
+ calendars: &'storages HashMap<String, Arc<dyn Storage<IcsItem>>>,
+ contacts: &'storages HashMap<String, Arc<dyn Storage<VcardItem>>>,
+ // TODO: the "previous state" is required here.
+ ) -> anyhow::Result<(
+ Vec<StoragePair<'storages, IcsItem>>,
+ Vec<StoragePair<'storages, VcardItem>>,
+ )> {
+ let mut calendar_pairs = Vec::new(); // TODO: with_capacity?
+ let mut contact_pairs = Vec::new(); // TODO: with_capacity?
+
+ for (name, source) in self.pairs.iter() {
+ match (calendars.get(&source.a), calendars.get(&source.b)) {
+ (None, None) => {
+ match (contacts.get(&source.a), contacts.get(&source.b)) {
+ (None, None) => {
+ bail!("Pair {} is missing both storages.", name);
+ }
+ (None, Some(_)) => {
+ bail!("Pair {} is missing contacts storage A.", name);
+ }
+ (Some(_), None) => {
+ bail!("Pair {} is missing contacts storage B.", name);
+ }
+ (Some(a), Some(b)) => {
+ contact_pairs.push(create_pair(name, source, a, b));
+ }
+ };
+ }
+ (None, Some(_)) => {
+ bail!("Pair {} is missing calendar storage A.", name);
+ }
+ (Some(_), None) => {
+ bail!("Pair {} is missing calendar storage B.", name);
+ }
+ (Some(a), Some(b)) => {
+ calendar_pairs.push(create_pair(name, source, a, b));
+ }
+ }
+ }
+ Ok((calendar_pairs, contact_pairs))
+ }
}
-impl GeneralSection {
- /// Returns the `status_path`, expanding a leading tilde if present.
- pub fn status_path(&self) -> Cow<Path> {
- let mut iter = self.status_path.as_path().as_os_str().as_bytes().iter();
- if let Some(b'~') = iter.next() {
- if let Some(b'/') = iter.next() {
- #[allow(deprecated)] // Only imperfect on unsupported platforms.
- let home = std::env::home_dir().expect("must resolve home path to expand tilde");
- let home = home.into_os_string().into_vec().into_iter();
- let all = home.chain(iter.copied());
- let os_string = OsString::from_vec(all.collect::<Vec<_>>());
- return Cow::Owned(PathBuf::from(os_string));
+fn create_pair<'a, I: Item>(
+ name: &str,
+ source: &PairSection,
+ a: &Arc<dyn Storage<I>>,
+ b: &Arc<dyn Storage<I>>,
+) -> StoragePair<'a, I> {
+ let mut pair = StoragePair::builder(a.clone(), b.clone());
+ for cv in &source.collections {
+ pair = match cv {
+ CollectionValue::All => pair.with_all_from_a().with_all_from_b(),
+ CollectionValue::FromA => pair.with_all_from_a(),
+ CollectionValue::FromB => pair.with_all_from_b(),
+ CollectionValue::Mapped(alias, a, b) => {
+ let mapping = DeclaredMapping::Mapped {
+ alias: alias.clone(),
+ a: a.to_description(),
+ b: b.to_description(),
+ };
+ pair.with_mapping(mapping)
}
+ CollectionValue::Collection(col) => pair.with_mapping(col.to_mapping()),
+ };
+ }
+ pair.build()
+}
+
+fn expand_tilde(orig: &PathBuf) -> Cow<Path> {
+ let mut iter = orig.as_path().as_os_str().as_bytes().iter();
+ if let Some(b'~') = iter.next() {
+ if let Some(b'/') = iter.next() {
+ #[allow(deprecated)] // Only problematic on unsupported platforms.
+ let home = std::env::home_dir().expect("must resolve home path to expand tilde");
+ let home = home.into_os_string().into_vec().into_iter();
+ let all = home.chain(std::iter::once(b'/')).chain(iter.copied());
+ let os_string = OsString::from_vec(all.collect::<Vec<_>>());
+ return Cow::Owned(PathBuf::from(os_string));
}
- Cow::Borrowed(&self.status_path)
}
+ Cow::Borrowed(&orig)
+}
+
+#[derive(Deserialize, Debug)]
+pub(crate) struct GeneralSection {
+ status_path: PathBuf,
}
#[derive(Deserialize, Debug)]
@@ 75,13 200,36 @@ enum CollectionValue {
}
#[derive(Deserialize, Debug)]
-pub enum Collection {
+enum Collection {
+ // TODO: can I re-use vstorage::sync::declare::CollectionDescription here?
#[serde(rename = "id")]
- Id(String),
+ Id(CollectionId),
#[serde(rename = "href")]
Href(String),
}
+impl Collection {
+ // TODO: these would be less inefficient if they consumed their input.
+
+ fn to_description(&self) -> CollectionDescription {
+ match self {
+ Collection::Id(id) => CollectionDescription::Id { id: id.clone() },
+ Collection::Href(href) => CollectionDescription::Href { href: href.clone() },
+ }
+ }
+
+ fn to_mapping(&self) -> DeclaredMapping {
+ match self {
+ Collection::Id(id) => DeclaredMapping::Direct {
+ description: CollectionDescription::Id { id: id.clone() },
+ },
+ Collection::Href(href) => DeclaredMapping::Direct {
+ description: CollectionDescription::Href { href: href.clone() },
+ },
+ }
+ }
+}
+
#[derive(Deserialize, Debug)]
enum CollectionSpecial {
#[serde(rename = "all")]
@@ 95,6 243,11 @@ enum CollectionSpecial {
#[derive(Deserialize, Debug)]
#[serde(tag = "type")]
enum StorageSection {
+ // TODO: a "protect" flag to protect one side if EVERYTHING is about to be deleted:
+ // - off
+ // - items: refuses to operate if any item would be deleted.
+ // - collection: refuses to operate if a non-empty collection would be emptied or deleted.
+ // - storage: refuses to operate if ALL collections would be emptied or deleted.
#[serde(rename = "filesystem/icalendar")]
FilesystemIcalendar(Filesystem<IcsItem>),
@@ 111,6 264,60 @@ enum StorageSection {
Http(Http),
}
+enum EitherStorage {
+ Calendar(Arc<dyn Storage<IcsItem>>),
+ AddressBook(Arc<dyn Storage<VcardItem>>),
+}
+
+impl StorageSection {
+ pub(crate) async fn storage(&self) -> anyhow::Result<EitherStorage> {
+ Ok(match self {
+ StorageSection::FilesystemIcalendar(def) => {
+ EitherStorage::Calendar(Arc::new(def.to_storage()))
+ }
+ StorageSection::FilesystemVcard(def) => {
+ EitherStorage::AddressBook(Arc::new(def.to_storage()))
+ }
+ StorageSection::CardDav(carddav) => {
+ EitherStorage::AddressBook(Arc::new(carddav.to_storage().await?))
+ }
+ StorageSection::CalDav(caldav) => {
+ EitherStorage::Calendar(Arc::new(caldav.to_storage().await?))
+ }
+ StorageSection::Http(http) => EitherStorage::Calendar(Arc::new(http.to_storage()?)),
+ })
+ }
+
+ // If this is a calendar, return the Storage.
+ //
+ // - Returns None if no storage matches this type.
+ // - Returns Some(_) if a storage matches.
+ pub(crate) async fn calendar_storage(
+ &self,
+ ) -> anyhow::Result<Option<Arc<dyn Storage<IcsItem>>>> {
+ Ok(match self {
+ StorageSection::FilesystemIcalendar(def) => Some(Arc::new(def.to_storage())),
+ StorageSection::FilesystemVcard(_) => None,
+ StorageSection::CardDav(_) => None,
+ StorageSection::CalDav(caldav) => Some(Arc::new(caldav.to_storage().await?)),
+ StorageSection::Http(http) => Some(Arc::new(http.to_storage()?)),
+ })
+ }
+
+ // If this is a calendar, return the Storage.
+ pub(crate) async fn contact_storage(
+ &self,
+ ) -> anyhow::Result<Option<Arc<dyn Storage<VcardItem>>>> {
+ Ok(match self {
+ StorageSection::FilesystemIcalendar(_) => None,
+ StorageSection::FilesystemVcard(def) => Some(Arc::new(def.to_storage())),
+ StorageSection::CardDav(carddav) => Some(Arc::new(carddav.to_storage().await?)),
+ StorageSection::CalDav(_) => None,
+ StorageSection::Http(_) => None,
+ })
+ }
+}
+
#[derive(Deserialize, Debug)]
struct Filesystem<I: Item> {
path: PathBuf,
@@ 123,13 330,35 @@ struct Filesystem<I: Item> {
item: PhantomData<I>,
}
+impl<I: Item> Filesystem<I> {
+ fn to_storage(&self) -> FilesystemStorage<I> {
+ let path = expand_tilde(&self.path);
+ FilesystemDefinition::new(path.to_owned().to_path_buf(), self.fileext.clone()).build()
+ }
+}
+
#[derive(Deserialize, Debug)]
struct CardDav {
url: String,
username: StringOrFetch,
password: StringOrFetch,
#[serde(flatten)]
- network_opts: NetworkOptions,
+ network_opts: HttpsConfig,
+}
+
+impl CardDav {
+ async fn to_storage(&self) -> anyhow::Result<CardDavStorage<HttpsConnector<HttpConnector>>> {
+ Ok(CardDavDefinition {
+ url: self.url.to_string().parse()?,
+ auth: libdav::auth::Auth::Basic {
+ username: self.username.to_string()?,
+ password: Some(self.password.to_password()?),
+ },
+ connector: self.network_opts.to_connector()?,
+ }
+ .build()
+ .await?)
+ }
}
#[derive(Deserialize, Debug)]
@@ 141,29 370,112 @@ struct CalDav {
// TODO: end_date
// TODO: item_types
#[serde(flatten)]
- network_opts: NetworkOptions,
+ network_opts: HttpsConfig,
+}
+
+impl CalDav {
+ async fn to_storage(&self) -> anyhow::Result<CalDavStorage<HttpsConnector<HttpConnector>>> {
+ Ok(CalDavDefinition {
+ url: self
+ .url
+ .to_string()?
+ .parse()
+ .context("parsing caldav URL")?,
+ auth: libdav::auth::Auth::Basic {
+ username: self.username.to_string()?,
+ password: Some(self.password.to_password()?),
+ },
+ connector: self.network_opts.to_connector()?,
+ }
+ .build()
+ .await?)
+ }
}
#[derive(Deserialize, Debug)]
-pub struct Http {
+pub(crate) struct Http {
url: StringOrFetch,
/// A name for the single collection inside this storage.
- collection: String,
+ collection: CollectionId,
#[serde(flatten)]
- network_opts: NetworkOptions,
+ https_config: HttpsConfig,
+}
+
+impl Http {
+ fn to_storage(&self) -> anyhow::Result<WebCalStorage> {
+ Ok(WebCalDefinition {
+ url: self.url.to_string()?.parse()?,
+ collection_name: self.collection.clone(),
+ }
+ .build()?)
+ }
}
#[derive(Deserialize, Debug)]
-struct NetworkOptions {
+struct HttpsConfig {
verify: Option<PathBuf>,
verify_fingerprint: Option<String>,
#[serde(default)]
auth: Auth,
- // TODO: auth_cert
+ auth_cert: Option<ClientCert>,
#[serde(default = "default_useragent")]
useragent: String,
}
+impl HttpsConfig {
+ fn to_connector(&self) -> anyhow::Result<HttpsConnector<HttpConnector>> {
+ let tls_config = ClientConfig::builder().with_safe_defaults();
+ let tls_config = match (&self.verify, &self.verify_fingerprint) {
+ (None, None) => tls_config
+ .with_native_roots()
+ .with_certificate_transparency_logs(&[], SystemTime::now()),
+ (None, Some(fingerprint)) => {
+ let verifier = Arc::from(FingerPrintVerifier::new(&fingerprint)?);
+ tls_config.with_custom_certificate_verifier(verifier)
+ }
+ (Some(path), None) => {
+ let mut root_store = RootCertStore::empty();
+ for cert in certs_from_pemfile(&path)? {
+ root_store.add(&cert)?;
+ }
+ tls_config
+ .with_root_certificates(root_store)
+ .with_certificate_transparency_logs(&[], SystemTime::now())
+ }
+ (Some(path), Some(fingerprint)) => {
+ let mut root_store = RootCertStore::empty();
+ for cert in certs_from_pemfile(&path)? {
+ root_store.add(&cert)?;
+ }
+ let verifier =
+ Arc::from(FingerPrintAndWebPkiVerifier::new(&fingerprint, root_store)?);
+ tls_config.with_custom_certificate_verifier(verifier)
+ }
+ };
+
+ let tls_config = match &self.auth_cert {
+ None => tls_config.with_no_client_auth(),
+ Some(cc) => {
+ let (certs, key) = match cc {
+ ClientCert::SingleFile(combined_path) => {
+ cert_and_key_from_pemfile(combined_path)?
+ }
+ ClientCert::SeparateKeyAndCert(crt_path, key_path) => {
+ (certs_from_pemfile(crt_path)?, key_from_pemfile(key_path)?)
+ }
+ };
+ tls_config.with_client_auth_cert(certs, key)?
+ }
+ };
+
+ Ok(HttpsConnectorBuilder::new()
+ .with_tls_config(tls_config)
+ .https_or_http()
+ .enable_http1()
+ .build())
+ }
+}
+
#[derive(Deserialize, Debug, Default)]
#[serde(rename_all = "lowercase")]
enum Auth {
@@ 177,30 489,52 @@ fn default_useragent() -> String {
String::from("vdirsyncer/2.0.0-alpha0") // FIXME: hard-coded version
}
+#[derive(Deserialize, Debug)]
+enum ClientCert {
+ SingleFile(PathBuf),
+ SeparateKeyAndCert(PathBuf, PathBuf),
+}
+
// TODO: singlefile
#[derive(Deserialize, Debug)]
#[serde(untagged)]
enum StringOrFetch {
Raw(String),
- Fetch(Fetch),
+ // TODO: evaluate whether I want 'shell' or 'prompt'.
+ Fetch { fetch: Vec<String> },
}
-#[derive(Deserialize, Debug)]
-struct Fetch {
- // Note: 'shell' and 'prompt' have been dropped.
- fetch: Vec<String>,
+impl StringOrFetch {
+ fn to_string(&self) -> anyhow::Result<String> {
+ match self {
+ StringOrFetch::Raw(s) => Ok(s.clone()),
+ StringOrFetch::Fetch { fetch } => {
+ // TODO: should expand user and normalise paths.
+ let mut values = fetch.iter();
+ if Some(&String::from("command")) != values.next() {
+ bail!("First word of a fetch directive must be 'command'")
+ };
+ let cmd = values.next().context("extracting command from 'fetch'")?;
+ let output = Command::new(cmd)
+ .args(values)
+ .stdout(Stdio::piped())
+ .output()
+ .context("executing fetch command")?;
+ Ok(std::str::from_utf8(&output.stdout)?.trim().to_owned())
+ }
+ }
+ }
+
+ fn to_password(&self) -> anyhow::Result<Password> {
+ self.to_string().map(Password::from)
+ }
}
-pub fn parse_from_file<P: AsRef<Path>>(path: P) -> anyhow::Result<Config> {
+/// Parse a configuration file at `path`.
+pub(crate) fn parse_from_file<P: AsRef<Path>>(path: P) -> anyhow::Result<Config> {
let raw = std::fs::read_to_string(path)?;
let config: Config = toml::from_str(&raw)?;
Ok(config)
}
-
-impl<I: Item> Filesystem<I> {
- fn build_storage(&self) -> FilesystemStorage<I> {
- todo!() // TODO
- }
-}
M vdirsyncer/src/main.rs => vdirsyncer/src/main.rs +1 -0
@@ 3,6 3,7 @@
// SPDX-License-Identifier: EUPL-1.2
mod config;
+mod tls;
fn main() -> anyhow::Result<()> {
let config = config::parse_from_file("/home/hugo/.config/vdirsyncer/config.toml")?;
A vdirsyncer/src/tls.rs => vdirsyncer/src/tls.rs +166 -0
@@ 0,0 1,166 @@
+// Copyright 2023 Hugo Osvaldo Barrera
+//
+// SPDX-License-Identifier: EUPL-1.2
+
+//! Helpers used for advanced TLS configuration.
+use std::{fs::File, io::BufReader, num::ParseIntError, path::Path, sync::Arc};
+
+use anyhow::{bail, Context};
+use rustls::{
+ client::{ServerCertVerified, ServerCertVerifier, WebPkiVerifier},
+ Certificate, CertificateError, PrivateKey, RootCertStore,
+};
+use sha2::{Digest, Sha256};
+
+/// Verifies that the fingerprint of a certificate matches.
+pub(crate) struct FingerPrintVerifier {
+ fingerprint: Vec<u8>,
+}
+
+impl FingerPrintVerifier {
+ // Create a new verifier from a hexadecimal fingerprint representation.
+ pub(crate) fn new(hex_fingerprint: &str) -> anyhow::Result<Self> {
+ let fingerprint = (0..hex_fingerprint.len())
+ .step_by(2)
+ .map(|i| u8::from_str_radix(&hex_fingerprint[i..=i + 1], 16))
+ .collect::<Result<Vec<u8>, ParseIntError>>()?;
+
+ Ok(FingerPrintVerifier { fingerprint })
+ }
+}
+
+impl ServerCertVerifier for FingerPrintVerifier {
+ fn verify_server_cert(
+ &self,
+ end_entity: &Certificate,
+ _intermediates: &[Certificate],
+ _server_name: &rustls::ServerName,
+ _scts: &mut dyn Iterator<Item = &[u8]>,
+ _ocsp_response: &[u8],
+ _now: std::time::SystemTime,
+ ) -> Result<rustls::client::ServerCertVerified, rustls::Error> {
+ let fingerprint = Sha256::digest(&end_entity.0).to_vec();
+
+ if self.fingerprint == fingerprint {
+ Ok(ServerCertVerified::assertion())
+ } else {
+ Err(rustls::Error::InvalidCertificate(CertificateError::Other(
+ Arc::from(FingerprintError),
+ )))
+ }
+ }
+}
+
+#[derive(Debug)]
+struct FingerprintError;
+
+impl std::fmt::Display for FingerprintError {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.write_str("certificate fingerprint does not match expectation")
+ }
+}
+
+impl std::error::Error for FingerprintError {}
+
+/// Verifies the fingerprint and CA for a certificate.
+pub(crate) struct FingerPrintAndWebPkiVerifier(FingerPrintVerifier, WebPkiVerifier);
+
+impl FingerPrintAndWebPkiVerifier {
+ pub(crate) fn new(
+ hex_fingerprint: &str,
+ roots: impl Into<Arc<RootCertStore>>,
+ ) -> anyhow::Result<Self> {
+ Ok(Self(
+ FingerPrintVerifier::new(hex_fingerprint)?,
+ WebPkiVerifier::new(roots, None),
+ ))
+ }
+}
+
+impl ServerCertVerifier for FingerPrintAndWebPkiVerifier {
+ fn verify_server_cert(
+ &self,
+ end_entity: &Certificate,
+ intermediates: &[Certificate],
+ server_name: &rustls::ServerName,
+ scts: &mut dyn Iterator<Item = &[u8]>,
+ ocsp_response: &[u8],
+ now: std::time::SystemTime,
+ ) -> Result<ServerCertVerified, rustls::Error> {
+ self.0.verify_server_cert(
+ end_entity,
+ intermediates,
+ server_name,
+ scts,
+ ocsp_response,
+ now,
+ )?;
+ self.1.verify_server_cert(
+ end_entity,
+ intermediates,
+ server_name,
+ scts,
+ ocsp_response,
+ now,
+ )
+ }
+}
+
+/// Load certificates from a PEM-encoded file.
+pub(crate) fn certs_from_pemfile(path: &Path) -> anyhow::Result<Vec<Certificate>> {
+ let mut reader = BufReader::new(File::open(path)?);
+ rustls_pemfile::certs(&mut reader)?
+ .into_iter()
+ .map(|v| Ok(Certificate(v)))
+ .collect()
+}
+
+/// Load a keyfile from a PEM-encoded file.
+pub(crate) fn key_from_pemfile(path: &Path) -> anyhow::Result<PrivateKey> {
+ let mut reader = BufReader::new(File::open(path)?);
+
+ loop {
+ match rustls_pemfile::read_one(&mut reader)? {
+ Some(
+ rustls_pemfile::Item::RSAKey(key)
+ | rustls_pemfile::Item::PKCS8Key(key)
+ | rustls_pemfile::Item::ECKey(key),
+ ) => return Ok(PrivateKey(key)),
+ None => break,
+ _ => {}
+ }
+ }
+
+ bail!("no keys found in {}", path.to_string_lossy());
+}
+
+/// Load certificates and a key file from a pem-encoded file.
+pub(crate) fn cert_and_key_from_pemfile(
+ path: &Path,
+) -> anyhow::Result<(Vec<Certificate>, PrivateKey)> {
+ let mut reader = BufReader::new(File::open(path)?);
+ let mut certs = Vec::new();
+ let mut raw_key = None;
+
+ loop {
+ match rustls_pemfile::read_one(&mut reader)? {
+ Some(
+ rustls_pemfile::Item::RSAKey(k)
+ | rustls_pemfile::Item::PKCS8Key(k)
+ | rustls_pemfile::Item::ECKey(k),
+ ) => {
+ if raw_key.replace(k).is_some() {
+ bail!("multiple keys found in {}", path.to_string_lossy());
+ }
+ }
+ None => break,
+ Some(rustls_pemfile::Item::X509Certificate(cert)) => certs.push(Certificate(cert)),
+ _ => {}
+ }
+ }
+
+ let key = raw_key
+ .map(|k| PrivateKey(k))
+ .with_context(|| format!("no key found in {}", path.to_string_lossy()))?;
+ Ok((certs, key))
+}