1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
// Copyright 2023 Hugo Osvaldo Barrera
//
// SPDX-License-Identifier: EUPL-1.2

//! Helpers for DNS-based discovery.

use std::io;
use std::string::FromUtf8Error;

use domain::base::name::LongChainError;
use domain::base::wire::ParseError;
use domain::base::ToRelativeDname;
use domain::resolv::lookup::srv::SrvError;
use domain::{
    base::{Dname, Question, RelativeDname, Rtype},
    rdata::Txt,
    resolv::StubResolver,
};
use http::uri::Scheme;

/// Services for which automatic discovery is possible.
#[derive(Debug, Clone, Copy)]
pub enum DiscoverableService {
    /// Caldav over HTTPS.
    CalDavs,
    /// Caldav over plain-text HTTP.
    CalDav,
    /// Carddav over HTTPS.
    CardDavs,
    /// Carddav over plain-text HTTP.
    CardDav,
}

impl DiscoverableService {
    /// Relative domain suitable for querying this service type.
    #[must_use]
    #[allow(clippy::missing_panics_doc)]
    pub fn relative_domain(self) -> &'static RelativeDname<[u8]> {
        match self {
            DiscoverableService::CalDavs => RelativeDname::from_slice(b"\x08_caldavs\x04_tcp"),
            DiscoverableService::CalDav => RelativeDname::from_slice(b"\x07_caldav\x04_tcp"),
            DiscoverableService::CardDavs => RelativeDname::from_slice(b"\x09_carddavs\x04_tcp"),
            DiscoverableService::CardDav => RelativeDname::from_slice(b"\x08_carddav\x04_tcp"),
        }
        .expect("well known relative prefix is valid")
    }

    /// The scheme for this service type (e.g.: HTTP or HTTPS).
    #[must_use]
    pub fn scheme(self) -> Scheme {
        match self {
            DiscoverableService::CalDavs | DiscoverableService::CardDavs => Scheme::HTTPS,
            DiscoverableService::CalDav | DiscoverableService::CardDav => Scheme::HTTP,
        }
    }

    /// The well-known path for context-path discovery.
    #[must_use]
    pub fn well_known_path(self) -> &'static str {
        match self {
            DiscoverableService::CalDavs | DiscoverableService::CalDav => "/.well-known/caldav",
            DiscoverableService::CardDavs | DiscoverableService::CardDav => "/.well-known/carddav",
        }
    }

    /// Returns the default port to try and use.
    #[must_use]
    pub fn default_port(self) -> u16 {
        match self {
            DiscoverableService::CalDavs | DiscoverableService::CardDavs => 443,
            DiscoverableService::CalDav | DiscoverableService::CardDav => 80,
        }
    }

    /// Value that must be present in the `DAV:` header when checking for support.
    ///
    /// # See also
    ///
    /// - <https://www.rfc-editor.org/rfc/rfc4791#section-5.1>
    /// - <https://www.rfc-editor.org/rfc/rfc6352#section-6.1>
    #[must_use]
    pub fn access_field(self) -> &'static str {
        match self {
            DiscoverableService::CalDavs | DiscoverableService::CardDavs => "calendar-access",
            DiscoverableService::CalDav | DiscoverableService::CardDav => "addressbook",
        }
    }
}

/// Resolves SRV to locate the caldav server.
///
/// If the query is successful and the service is available, returns `Ok(Some(_))` with a `Vec` of
/// host/ports, in the order in which they should be tried.
///
/// If the query is successful but the service is decidedly not available, returns `Ok(None)`.
///
/// # Errors
///
/// If the underlying DNS request fails or the SRV record cannot be parsed.
///
/// # See also
///
/// - <https://www.rfc-editor.org/rfc/rfc2782>
/// - <https://www.rfc-editor.org/rfc/rfc6764>
pub async fn resolve_srv_record(
    service: DiscoverableService,
    domain: &Dname<impl AsRef<[u8]>>,
    port: u16,
) -> Result<Option<Vec<(String, u16)>>, SrvError> {
    Ok(StubResolver::new()
        .lookup_srv(service.relative_domain(), domain, port)
        .await?
        .map(|found| {
            found
                .into_srvs()
                .map(|entry| (entry.target().to_string(), entry.port()))
                .collect()
        }))
}

/// Error returned by [`find_context_path_via_txt_records`].
#[derive(thiserror::Error, Debug)]
pub enum TxtError {
    #[error("I/O error performing DNS request")]
    Network(#[from] io::Error),

    #[error("the domain name is too long and cannot be queried")]
    DomainTooLong(#[from] LongChainError),

    #[error("error parsing DNS response")]
    ParseError(#[from] ParseError),

    #[error("txt record does not contain a valid utf-8 string")]
    NotUtf8Error(#[from] FromUtf8Error),

    #[error("data in txt record does no have the right syntax")]
    BadTxt,
}

impl From<TxtError> for io::Error {
    fn from(value: TxtError) -> Self {
        match value {
            TxtError::Network(err) => err,
            TxtError::DomainTooLong(_) => io::Error::new(io::ErrorKind::InvalidInput, value),
            TxtError::ParseError(_) | TxtError::NotUtf8Error(_) | TxtError::BadTxt => {
                io::Error::new(io::ErrorKind::InvalidData, value)
            }
        }
    }
}

/// Resolves a context path via TXT records.
///
/// This returns a path where the default context path should be used for a given domain.
/// The domain provided should be in the format of `example.com` or `posteo.de`.
///
/// Returns an empty list of no relevant record was found.
///
/// # Errors
///
/// See [`TxtError`]
///
/// # See also
///
/// <https://www.rfc-editor.org/rfc/rfc6764>
pub async fn find_context_path_via_txt_records(
    service: DiscoverableService,
    domain: &Dname<impl AsRef<[u8]>>,
) -> Result<Option<String>, TxtError> {
    let resolver = StubResolver::new();
    let full_domain = service.relative_domain().chain(domain)?;
    let question = Question::new_in(full_domain, Rtype::Txt);

    let response = resolver.query(question).await?;
    let Some(record) = response.answer()?.next() else {
        return Ok(None);
    };
    let Some(parsed_record) = record?.into_record::<Txt<_>>()? else {
        return Ok(None);
    };

    let bytes = parsed_record.data().text::<Vec<u8>>();

    let path_result = String::from_utf8(bytes)?
        .strip_prefix("path=")
        .ok_or(TxtError::BadTxt)
        .map(String::from);
    Some(path_result).transpose()
}