dns_update/
lib.rs

1#![doc = include_str!("../README.md")]
2use core::fmt;
3/*
4 * Copyright Stalwart Labs LLC See the COPYING
5 * file at the top-level directory of this distribution.
6 *
7 * Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
8 * https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
9 * <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
10 * option. This file may not be copied, modified, or distributed
11 * except according to those terms.
12 */
13use std::{
14    borrow::Cow,
15    fmt::{Display, Formatter},
16    net::{Ipv4Addr, Ipv6Addr},
17    str::FromStr,
18    time::Duration,
19};
20
21use hickory_client::proto::rr::dnssec::{KeyPair, Private};
22
23use providers::{
24    cloudflare::CloudflareProvider,
25    desec::DesecProvider,
26    digitalocean::DigitalOceanProvider,
27    ovh::{OvhProvider, OvhEndpoint},
28    rfc2136::{DnsAddress, Rfc2136Provider},
29};
30
31pub mod http;
32pub mod providers;
33pub mod tests;
34
35#[derive(Debug)]
36pub enum Error {
37    Protocol(String),
38    Parse(String),
39    Client(String),
40    Response(String),
41    Api(String),
42    Serialize(String),
43    Unauthorized,
44    NotFound,
45    BadRequest,
46}
47
48/// A DNS record type.
49#[derive(Debug)]
50pub enum DnsRecordType {
51    A,
52    AAAA,
53    CNAME,
54    NS,
55    MX,
56    TXT,
57    SRV,
58}
59
60/// A DNS record type with a value.
61pub enum DnsRecord {
62    A {
63        content: Ipv4Addr,
64    },
65    AAAA {
66        content: Ipv6Addr,
67    },
68    CNAME {
69        content: String,
70    },
71    NS {
72        content: String,
73    },
74    MX {
75        content: String,
76        priority: u16,
77    },
78    TXT {
79        content: String,
80    },
81    SRV {
82        content: String,
83        priority: u16,
84        weight: u16,
85        port: u16,
86    },
87}
88
89/// A TSIG algorithm.
90pub enum TsigAlgorithm {
91    HmacMd5,
92    Gss,
93    HmacSha1,
94    HmacSha224,
95    HmacSha256,
96    HmacSha256_128,
97    HmacSha384,
98    HmacSha384_192,
99    HmacSha512,
100    HmacSha512_256,
101}
102
103/// A DNSSEC algorithm.
104pub enum Algorithm {
105    RSASHA256,
106    RSASHA512,
107    ECDSAP256SHA256,
108    ECDSAP384SHA384,
109    ED25519,
110}
111
112pub type Result<T> = std::result::Result<T, Error>;
113
114#[derive(Clone)]
115#[non_exhaustive]
116pub enum DnsUpdater {
117    Rfc2136(Rfc2136Provider),
118    Cloudflare(CloudflareProvider),
119    DigitalOcean(DigitalOceanProvider),
120    Desec(DesecProvider),
121    Ovh(OvhProvider),
122}
123
124pub trait IntoFqdn<'x> {
125    fn into_fqdn(self) -> Cow<'x, str>;
126    fn into_name(self) -> Cow<'x, str>;
127}
128
129impl DnsUpdater {
130    /// Create a new DNS updater using the RFC 2136 protocol and TSIG authentication.
131    pub fn new_rfc2136_tsig(
132        addr: impl TryInto<DnsAddress>,
133        key_name: impl AsRef<str>,
134        key: impl Into<Vec<u8>>,
135        algorithm: TsigAlgorithm,
136    ) -> crate::Result<Self> {
137        Ok(DnsUpdater::Rfc2136(Rfc2136Provider::new_tsig(
138            addr,
139            key_name,
140            key,
141            algorithm.into(),
142        )?))
143    }
144
145    /// Create a new DNS updater using the RFC 2136 protocol and SIG(0) authentication.
146    pub fn new_rfc2136_sig0(
147        addr: impl TryInto<DnsAddress>,
148        signer_name: impl AsRef<str>,
149        key: KeyPair<Private>,
150        public_key: impl Into<Vec<u8>>,
151        algorithm: Algorithm,
152    ) -> crate::Result<Self> {
153        Ok(DnsUpdater::Rfc2136(Rfc2136Provider::new_sig0(
154            addr,
155            signer_name,
156            key,
157            public_key,
158            algorithm.into(),
159        )?))
160    }
161
162    /// Create a new DNS updater using the Cloudflare API.
163    pub fn new_cloudflare(
164        secret: impl AsRef<str>,
165        email: Option<impl AsRef<str>>,
166        timeout: Option<Duration>,
167    ) -> crate::Result<Self> {
168        Ok(DnsUpdater::Cloudflare(CloudflareProvider::new(
169            secret, email, timeout,
170        )?))
171    }
172
173    /// Create a new DNS updater using the Cloudflare API.
174    pub fn new_digitalocean(
175        auth_token: impl AsRef<str>,
176        timeout: Option<Duration>,
177    ) -> crate::Result<Self> {
178        Ok(DnsUpdater::DigitalOcean(DigitalOceanProvider::new(
179            auth_token, timeout,
180        )))
181    }
182
183    /// Create a new DNS updater using the Desec.io API.
184    pub fn new_desec(
185        auth_token: impl AsRef<str>,
186        timeout: Option<Duration>,
187    ) -> crate::Result<Self> {
188        Ok(DnsUpdater::Desec(DesecProvider::new(auth_token, timeout)))
189    }
190
191    /// Create a new DNS updater using the OVH API.
192    pub fn new_ovh(
193        application_key: impl AsRef<str>,
194        application_secret: impl AsRef<str>,
195        consumer_key: impl AsRef<str>,
196        endpoint: OvhEndpoint,
197        timeout: Option<Duration>,
198    ) -> crate::Result<Self> {
199        Ok(DnsUpdater::Ovh(OvhProvider::new(
200            application_key,
201            application_secret,
202            consumer_key,
203            endpoint,
204            timeout,
205        )?))
206    }
207
208    /// Create a new DNS record.
209    pub async fn create(
210        &self,
211        name: impl IntoFqdn<'_>,
212        record: DnsRecord,
213        ttl: u32,
214        origin: impl IntoFqdn<'_>,
215    ) -> crate::Result<()> {
216        match self {
217            DnsUpdater::Rfc2136(provider) => provider.create(name, record, ttl, origin).await,
218            DnsUpdater::Cloudflare(provider) => provider.create(name, record, ttl, origin).await,
219            DnsUpdater::DigitalOcean(provider) => provider.create(name, record, ttl, origin).await,
220            DnsUpdater::Desec(provider) => provider.create(name, record, ttl, origin).await,
221            DnsUpdater::Ovh(provider) => provider.create(name, record, ttl, origin).await,
222        }
223    }
224
225    /// Update an existing DNS record.
226    pub async fn update(
227        &self,
228        name: impl IntoFqdn<'_>,
229        record: DnsRecord,
230        ttl: u32,
231        origin: impl IntoFqdn<'_>,
232    ) -> crate::Result<()> {
233        match self {
234            DnsUpdater::Rfc2136(provider) => provider.update(name, record, ttl, origin).await,
235            DnsUpdater::Cloudflare(provider) => provider.update(name, record, ttl, origin).await,
236            DnsUpdater::DigitalOcean(provider) => provider.update(name, record, ttl, origin).await,
237            DnsUpdater::Desec(provider) => provider.update(name, record, ttl, origin).await,
238            DnsUpdater::Ovh(provider) => provider.update(name, record, ttl, origin).await,
239        }
240    }
241
242    /// Delete an existing DNS record.
243    pub async fn delete(
244        &self,
245        name: impl IntoFqdn<'_>,
246        origin: impl IntoFqdn<'_>,
247        record: DnsRecordType,
248    ) -> crate::Result<()> {
249        match self {
250            DnsUpdater::Rfc2136(provider) => provider.delete(name, origin).await,
251            DnsUpdater::Cloudflare(provider) => provider.delete(name, origin).await,
252            DnsUpdater::DigitalOcean(provider) => provider.delete(name, origin).await,
253            DnsUpdater::Desec(provider) => provider.delete(name, origin, record).await,
254            DnsUpdater::Ovh(provider) => provider.delete(name, origin, record).await,
255        }
256    }
257}
258
259impl<'x> IntoFqdn<'x> for &'x str {
260    fn into_fqdn(self) -> Cow<'x, str> {
261        if self.ends_with('.') {
262            Cow::Borrowed(self)
263        } else {
264            Cow::Owned(format!("{}.", self))
265        }
266    }
267
268    fn into_name(self) -> Cow<'x, str> {
269        if let Some(name) = self.strip_suffix('.') {
270            Cow::Borrowed(name)
271        } else {
272            Cow::Borrowed(self)
273        }
274    }
275}
276
277impl<'x> IntoFqdn<'x> for &'x String {
278    fn into_fqdn(self) -> Cow<'x, str> {
279        self.as_str().into_fqdn()
280    }
281
282    fn into_name(self) -> Cow<'x, str> {
283        self.as_str().into_name()
284    }
285}
286
287impl<'x> IntoFqdn<'x> for String {
288    fn into_fqdn(self) -> Cow<'x, str> {
289        if self.ends_with('.') {
290            Cow::Owned(self)
291        } else {
292            Cow::Owned(format!("{}.", self))
293        }
294    }
295
296    fn into_name(self) -> Cow<'x, str> {
297        if let Some(name) = self.strip_suffix('.') {
298            Cow::Owned(name.to_string())
299        } else {
300            Cow::Owned(self)
301        }
302    }
303}
304
305pub fn strip_origin_from_name(name: &str, origin: &str) -> String {
306    let name = name.trim_end_matches('.');
307    let origin = origin.trim_end_matches('.');
308
309    if name == origin {
310        return "@".to_string();
311    }
312
313    if name.ends_with(&format!(".{}", origin)) {
314        name[..name.len() - origin.len() - 1].to_string()
315    } else {
316        name.to_string()
317    }
318}
319
320impl FromStr for TsigAlgorithm {
321    type Err = ();
322
323    fn from_str(s: &str) -> std::prelude::v1::Result<Self, Self::Err> {
324        match s {
325            "hmac-md5" => Ok(TsigAlgorithm::HmacMd5),
326            "gss" => Ok(TsigAlgorithm::Gss),
327            "hmac-sha1" => Ok(TsigAlgorithm::HmacSha1),
328            "hmac-sha224" => Ok(TsigAlgorithm::HmacSha224),
329            "hmac-sha256" => Ok(TsigAlgorithm::HmacSha256),
330            "hmac-sha256-128" => Ok(TsigAlgorithm::HmacSha256_128),
331            "hmac-sha384" => Ok(TsigAlgorithm::HmacSha384),
332            "hmac-sha384-192" => Ok(TsigAlgorithm::HmacSha384_192),
333            "hmac-sha512" => Ok(TsigAlgorithm::HmacSha512),
334            "hmac-sha512-256" => Ok(TsigAlgorithm::HmacSha512_256),
335            _ => Err(()),
336        }
337    }
338}
339
340impl Display for Error {
341    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
342        match self {
343            Error::Protocol(e) => write!(f, "Protocol error: {}", e),
344            Error::Parse(e) => write!(f, "Parse error: {}", e),
345            Error::Client(e) => write!(f, "Client error: {}", e),
346            Error::Response(e) => write!(f, "Response error: {}", e),
347            Error::Api(e) => write!(f, "API error: {}", e),
348            Error::Serialize(e) => write!(f, "Serialize error: {}", e),
349            Error::Unauthorized => write!(f, "Unauthorized"),
350            Error::NotFound => write!(f, "Not found"),
351            Error::BadRequest => write!(f, "Bad request"),
352        }
353    }
354}
355
356impl Display for DnsRecordType {
357    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
358        write!(f, "{:?}", self)
359    }
360}