libdav/
carddav.rs

1// Copyright 2023-2024 Hugo Osvaldo Barrera
2//
3// SPDX-License-Identifier: ISC
4
5use std::ops::Deref;
6
7use http::Response;
8use hyper::Uri;
9use hyper::body::Incoming;
10use tower_service::Service;
11
12use crate::common::{ServiceForUrlError, check_support, parse_find_multiple_collections};
13use crate::dav::WebDavClient;
14use crate::dav::{FoundCollection, WebDavError, check_status};
15use crate::sd::{BootstrapError, DiscoverableService, find_context_url};
16use crate::xmlutils::escape_xml_entities;
17use crate::{CheckSupportError, FetchedResource};
18use crate::{Depth, FindHomeSetError, names};
19
20/// Client to communicate with a CardDAV server.
21///
22/// Instances are usually created via [`CardDavClient::new`]:
23///
24/// ```rust
25/// # use libdav::CardDavClient;
26/// # use libdav::dav::WebDavClient;
27/// use http::Uri;
28/// use hyper_rustls::HttpsConnectorBuilder;
29/// use hyper_util::{client::legacy::Client, rt::TokioExecutor};
30/// use tower_http::auth::AddAuthorization;
31///
32/// # tokio::runtime::Builder::new_current_thread().build().unwrap().block_on(async {
33/// let uri = Uri::try_from("https://example.com").unwrap();
34///
35/// let https_connector = HttpsConnectorBuilder::new()
36///     .with_native_roots()
37///     .unwrap()
38///     .https_or_http()
39///     .enable_http1()
40///     .build();
41/// let https_client = Client::builder(TokioExecutor::new()).build(https_connector);
42/// let https_client = AddAuthorization::basic(https_client, "user", "secret");
43/// let webdav = WebDavClient::new(uri, https_client);
44/// let client = CardDavClient::new(webdav);
45/// # })
46/// ```
47///
48/// If the real CardDAV server needs to be resolved via automated service discovery, use
49/// [`CardDavClient::bootstrap_via_service_discovery`].
50///
51/// For setting the `Authorization` header or applying other changes to outgoing requests, see the
52/// documentation on [`WebDavClient`].
53#[derive(Debug)]
54pub struct CardDavClient<C>
55where
56    C: Service<http::Request<String>, Response = Response<Incoming>> + Sync + Send + 'static,
57{
58    /// A WebDAV client used to send requests.
59    pub webdav_client: WebDavClient<C>,
60}
61
62impl<C> Deref for CardDavClient<C>
63where
64    C: Service<http::Request<String>, Response = Response<Incoming>> + Sync + Send,
65{
66    type Target = WebDavClient<C>;
67
68    fn deref(&self) -> &Self::Target {
69        &self.webdav_client
70    }
71}
72
73impl<C> CardDavClient<C>
74where
75    C: Service<http::Request<String>, Response = Response<Incoming>> + Sync + Send,
76    <C as Service<http::Request<String>>>::Error: std::error::Error + Send + Sync,
77{
78    /// Create a new client instance.
79    ///
80    /// # See also
81    ///
82    /// [`CardDavClient::bootstrap_via_service_discovery`].
83    pub fn new(webdav_client: WebDavClient<C>) -> CardDavClient<C> {
84        CardDavClient { webdav_client }
85    }
86
87    /// Automatically bootstrap a new client instance.
88    ///
89    /// Creates a new client, with its `base_url` set to the context path retrieved using service
90    /// discovery via [`find_context_url`].
91    ///
92    /// # Errors
93    ///
94    /// Returns an error if:
95    ///
96    /// - The URL has an invalid schema.
97    /// - The underlying call to [`find_context_url`] returns an error.
98    pub async fn bootstrap_via_service_discovery(
99        mut webdav_client: WebDavClient<C>,
100    ) -> Result<CardDavClient<C>, BootstrapError> {
101        let service = service_for_url(&webdav_client.base_url)?;
102        match find_context_url(&webdav_client, service).await {
103            crate::sd::FindContextUrlResult::BaseUrl => {}
104            crate::sd::FindContextUrlResult::Found(url) => webdav_client.base_url = url,
105            crate::sd::FindContextUrlResult::NoneFound => return Err(BootstrapError::NoUsableUrl),
106            crate::sd::FindContextUrlResult::Error(err) => return Err(err.into()),
107        }
108        Ok(CardDavClient { webdav_client })
109    }
110
111    /// Queries the server for the address book home set.
112    ///
113    /// See: <https://www.rfc-editor.org/rfc/rfc4791#section-6.2.1>
114    ///
115    /// # Errors
116    ///
117    /// If there are any network errors or the response could not be parsed.
118    pub async fn find_address_book_home_set(
119        &self,
120        principal: &Uri,
121    ) -> Result<Vec<Uri>, FindHomeSetError> {
122        // If obtaining a principal fails, the specification says we should query the user. This
123        // tries to use the `base_url` first, since the user might have provided it for a reason.
124        self.find_home_sets(principal, &names::ADDRESSBOOK_HOME_SET)
125            .await
126            .map_err(FindHomeSetError)
127    }
128
129    /// Find address book collections under the given `url`.
130    ///
131    /// If `url` is not specified, this client's current `base_url` shall be used instead.  When
132    /// using a client bootstrapped via automatic discovery, passing `None` will usually yield the
133    /// expected results.
134    ///
135    /// # Errors
136    ///
137    /// If the HTTP call fails or parsing the XML response fails.
138    pub async fn find_addressbooks(
139        &self,
140        address_book_home_set: &Uri,
141    ) -> Result<Vec<FoundCollection>, WebDavError> {
142        let props = [
143            &names::RESOURCETYPE,
144            &names::GETETAG,
145            &names::SUPPORTED_REPORT_SET,
146        ];
147        let (head, body) = self
148            .propfind(address_book_home_set, &props, Depth::One)
149            .await?;
150        check_status(head.status)?;
151
152        parse_find_multiple_collections(&body, &names::ADDRESSBOOK)
153    }
154
155    /// Fetches existing vcard resources.
156    ///
157    /// If the `getetag` property is missing for an item, it will be reported as
158    /// [`http::StatusCode::NOT_FOUND`]. This should not be an actual issue with in practice, since
159    /// support for `getetag` is mandatory for CalDAV implementations.
160    ///
161    /// # Errors
162    ///
163    /// If there are any network errors or the response could not be parsed.
164    pub async fn get_address_book_resources(
165        &self,
166        addressbook_href: &str,
167        hrefs: impl IntoIterator<Item = impl AsRef<str>>,
168    ) -> Result<Vec<FetchedResource>, WebDavError> {
169        let mut body = String::from(
170            r#"
171            <C:addressbook-multiget xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:carddav">
172                <D:prop>
173                    <D:getetag/>
174                    <C:address-data/>
175                </D:prop>"#,
176        );
177        for href in hrefs {
178            body.push_str("<D:href>");
179            body.push_str(&escape_xml_entities(href.as_ref()));
180            body.push_str("</D:href>");
181        }
182        body.push_str("</C:addressbook-multiget>");
183
184        self.multi_get(addressbook_href.as_ref(), body, &names::ADDRESS_DATA)
185            .await
186    }
187
188    /// Checks that the given URI advertises carddav support.
189    ///
190    /// See: <https://www.rfc-editor.org/rfc/rfc6352#section-6.1>
191    ///
192    /// # Known Issues
193    ///
194    /// - Nextcloud's implementation seems broken. [Bug report][nextcloud].
195    ///
196    /// [nextcloud]: https://github.com/nextcloud/server/issues/37374
197    ///
198    /// # Errors
199    ///
200    /// If there are any network issues or if the server does not explicitly advertise carddav
201    /// support.
202    pub async fn check_support(&self, url: &Uri) -> Result<(), CheckSupportError> {
203        check_support(&self.webdav_client, url, "addressbook").await
204    }
205
206    /// Create an address book collection.
207    ///
208    /// # Errors
209    ///
210    /// Returns an error in case of network errors or if the server returns a failure status code.
211    pub async fn create_addressbook(&self, href: &str) -> Result<(), WebDavError> {
212        self.webdav_client
213            .create_collection(href, &[&names::ADDRESSBOOK])
214            .await
215    }
216}
217
218impl<C> Clone for CardDavClient<C>
219where
220    C: Service<http::Request<String>, Response = Response<Incoming>> + Sync + Send + Clone,
221{
222    fn clone(&self) -> CardDavClient<C> {
223        CardDavClient {
224            webdav_client: self.webdav_client.clone(),
225        }
226    }
227}
228
229/// Return the service type based on a URL's scheme.
230///
231/// # Errors
232///
233/// If `url` is missing a scheme or has a scheme invalid for CardDAV usage.
234pub fn service_for_url(url: &Uri) -> Result<DiscoverableService, ServiceForUrlError> {
235    match url
236        .scheme()
237        .ok_or(ServiceForUrlError::MissingScheme)?
238        .as_ref()
239    {
240        "https" | "carddavs" => Ok(DiscoverableService::CardDavs),
241        "http" | "carddav" => Ok(DiscoverableService::CardDav),
242        _ => Err(ServiceForUrlError::UnknownScheme),
243    }
244}