1#![warn(missing_docs)]
2mod serde_util;
47
48pub mod transport;
49#[cfg(feature = "default-client")]
50use transport::DefaultTransport;
51use transport::MakeRequest;
52mod error;
53pub use error::Error;
54use error::{ApiErrorMessage, ApiResponse, ErrorImpl};
55mod uri;
56
57use chrono::NaiveDateTime;
58use http_body_util::{BodyExt, Full};
59use hyper::{
60 body::{Body, Bytes},
61 Request, StatusCode, Uri,
62};
63use serde::{Deserialize, Serialize};
64use std::{
65 borrow::Cow,
66 collections::HashMap,
67 fmt::Display,
68 net::{IpAddr, Ipv4Addr, Ipv6Addr},
69 time::Duration,
70};
71
72#[derive(Deserialize, Serialize, Clone)]
74pub struct ApiKey {
75 secretapikey: String,
76 apikey: String,
77}
78
79impl ApiKey {
80 pub fn new(secret: impl Into<String>, api_key: impl Into<String>) -> Self {
82 Self {
83 secretapikey: secret.into(),
84 apikey: api_key.into(),
85 }
86 }
87}
88
89#[allow(missing_docs)]
91#[derive(Clone, Copy, Deserialize, Serialize, Debug, PartialEq, Eq)]
92pub enum DnsRecordType {
93 A,
94 MX,
95 CNAME,
96 ALIAS,
97 TXT,
98 NS,
99 AAAA,
100 SRV,
101 TLSA,
102 CAA,
103 HTTPS,
104 SVCB,
105}
106
107impl Display for DnsRecordType {
108 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109 f.write_str(match self {
110 Self::A => "A",
111 Self::MX => "MX",
112 Self::CNAME => "CNAME",
113 Self::ALIAS => "ALIAS",
114 Self::TXT => "TXT",
115 Self::NS => "NS",
116 Self::AAAA => "AAAA",
117 Self::SRV => "SRV",
118 Self::TLSA => "TLSA",
119 Self::CAA => "CAA",
120 Self::HTTPS => "HTTPS",
121 Self::SVCB => "SVCB",
122 })
123 }
124}
125
126#[derive(Debug, Serialize, PartialEq, Eq)]
128pub struct CreateOrEditDnsRecord<'a> {
129 #[serde(rename = "name")]
131 pub subdomain: Option<&'a str>,
132 #[serde(rename = "type")]
134 pub record_type: DnsRecordType,
135 pub content: Cow<'a, str>,
137 pub ttl: Option<u64>,
139 pub prio: u32,
142 }
146
147impl<'a> CreateOrEditDnsRecord<'a> {
148 pub fn new(
150 subdomain: Option<&'a str>,
151 record_type: DnsRecordType,
152 content: impl Into<Cow<'a, str>>,
153 ) -> Self {
154 Self {
155 subdomain,
156 record_type,
157 content: content.into(),
158 ttl: None,
159 prio: 0,
160 }
161 }
162 #[allow(non_snake_case)]
164 pub fn A(subdomain: Option<&'a str>, ip: Ipv4Addr) -> Self {
165 Self::new(subdomain, DnsRecordType::A, Cow::Owned(ip.to_string()))
166 }
167 #[allow(non_snake_case)]
169 pub fn AAAA(subdomain: Option<&'a str>, ip: Ipv6Addr) -> Self {
170 Self::new(subdomain, DnsRecordType::AAAA, Cow::Owned(ip.to_string()))
171 }
172 #[allow(non_snake_case)]
174 pub fn A_or_AAAA(subdomain: Option<&'a str>, ip: IpAddr) -> Self {
175 match ip {
176 IpAddr::V4(my_ip) => Self::A(subdomain, my_ip),
177 IpAddr::V6(my_ip) => Self::AAAA(subdomain, my_ip),
178 }
179 }
180
181 #[must_use]
184 pub fn with_ttl(self, ttl: Option<Duration>) -> Self {
185 Self {
186 ttl: ttl.as_ref().map(Duration::as_secs),
187 ..self
188 }
189 }
190
191 #[must_use]
193 pub fn with_priority(self, prio: u32) -> Self {
194 Self { prio, ..self }
195 }
196}
197
198#[derive(Deserialize, Debug)]
203struct EntryId {
204 #[serde(with = "serde_util::string_or_int")]
205 id: String,
206}
207
208#[derive(Deserialize, Debug)]
210pub struct DnsEntry {
211 #[serde(with = "serde_util::string_or_int")]
213 pub id: String,
214 pub name: String,
216 #[serde(rename = "type")]
218 pub record_type: DnsRecordType,
219 pub content: String,
221 #[serde(with = "serde_util::u64_from_string_or_int")]
224 pub ttl: u64,
225 #[serde(default, with = "serde_util::u64_from_string_or_int")]
228 pub prio: u64,
229 pub notes: Option<String>,
232}
233
234#[derive(Deserialize, Debug)]
235struct DnsRecordsByDomainOrIDResponse {
236 records: Vec<DnsEntry>,
237}
238
239#[derive(Deserialize, Debug)]
241#[serde(rename_all = "camelCase")]
242pub struct Pricing {
243 pub registration: String,
245 pub renewal: String,
247 pub transfer: String,
249 pub special_type: TldType,
256}
257
258impl Pricing {
259 pub fn is_icann(&self) -> bool {
261 self.special_type.is_icann()
262 }
263}
264
265#[derive(Debug)]
267pub enum TldType {
268 Normal,
271 Handshake,
273 Other(String),
277}
278
279impl TldType {
280 pub fn is_icann(&self) -> bool {
282 matches!(self, Self::Normal)
283 }
284}
285
286impl<'de> Deserialize<'de> for TldType {
287 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
288 where
289 D: serde::Deserializer<'de>,
290 {
291 let string_value = Option::<String>::deserialize(deserializer)?;
292 Ok(match string_value {
293 None => Self::Normal,
294 Some(s) => {
295 if s.eq_ignore_ascii_case("handshake") {
296 Self::Handshake
297 } else {
298 Self::Other(s)
299 }
300 }
301 })
302 }
303}
304
305#[derive(Deserialize, Debug)]
306struct DomainPricingResponse {
307 pricing: HashMap<String, Pricing>,
309}
310
311#[derive(Serialize, Deserialize)]
312struct UpdateNameServers {
313 ns: Vec<String>,
314}
315
316#[derive(Serialize)]
317#[serde(rename_all = "camelCase")]
318struct DomainListAll {
319 start: usize,
321 #[serde(default, with = "serde_util::yesno")]
323 include_labels: bool,
324}
325
326#[derive(Deserialize, Debug)]
327#[serde(rename_all = "camelCase")]
328struct DomainListAllResponse {
329 domains: Vec<DomainInfo>,
330}
331
332#[derive(Deserialize, Debug)]
334#[serde(rename_all = "camelCase")]
335pub struct DomainInfo {
336 pub domain: String,
338 pub status: String,
341 pub tld: String,
343 #[serde(with = "serde_util::datetime")]
346 pub create_date: NaiveDateTime,
347 #[serde(with = "serde_util::datetime")]
349 pub expire_date: NaiveDateTime,
350 #[serde(with = "serde_util::stringoneintzero")]
353 pub security_lock: bool,
354 #[serde(with = "serde_util::stringoneintzero")]
355 pub whois_privacy: bool,
357 #[serde(with = "serde_util::stringoneintzero")]
360 pub auto_renew: bool,
361 #[serde(with = "serde_util::stringoneintzero")]
363 pub not_local: bool,
364 #[serde(default)]
366 pub labels: Vec<Label>,
367}
368
369#[derive(Deserialize, Debug)]
371#[serde(rename_all = "camelCase")]
372pub struct Label {
373 #[serde(deserialize_with = "serde_util::string_or_int::deserialize")]
375 pub id: String,
376 pub title: String,
378 pub color: String,
380}
381
382#[derive(Serialize, Deserialize, Debug)]
384#[serde(rename_all = "camelCase")]
385pub struct Forward {
386 #[serde(skip_serializing_if = "Option::is_none")]
388 pub subdomain: Option<String>,
389 pub location: String,
391 #[serde(rename = "type")]
392 pub forward_type: ForwardType,
394 #[serde(with = "serde_util::yesno")]
396 pub include_path: bool,
397 #[serde(with = "serde_util::yesno")]
399 pub wildcard: bool,
400}
401
402impl Forward {
403 pub fn new(subdomain: Option<impl Into<String>>, to: impl Into<String>) -> Self {
406 Self {
407 subdomain: subdomain.map(|s| s.into()),
408 location: to.into(),
409 forward_type: ForwardType::Temporary,
410 include_path: true,
411 wildcard: false,
412 }
413 }
414
415 pub fn with_wildcard(self, wildcard: bool) -> Self {
417 Self { wildcard, ..self }
418 }
419
420 pub fn include_path(self, include_path: bool) -> Self {
422 Self {
423 include_path,
424 ..self
425 }
426 }
427
428 pub fn with_forward_type(self, forward_type: ForwardType) -> Self {
430 Self {
431 forward_type,
432 ..self
433 }
434 }
435}
436
437#[allow(missing_docs)]
439#[derive(Deserialize, Serialize, Debug)]
440#[serde(rename_all = "lowercase")]
441pub enum ForwardType {
442 Temporary,
443 Permanent,
444}
445
446#[derive(Deserialize, Debug)]
447#[serde(rename_all = "camelCase")]
448struct GetUrlForwardingResponse {
449 forwards: Vec<ForwardWithID>,
450}
451
452#[derive(Deserialize, Debug)]
454#[serde(rename_all = "camelCase")]
455pub struct ForwardWithID {
456 #[serde(deserialize_with = "serde_util::string_or_int::deserialize")]
458 pub id: String,
459 #[serde(flatten, rename = "forward")]
461 pub config: Forward,
462}
463
464#[derive(Deserialize, Debug)]
466pub struct SslBundle {
467 #[serde(rename = "certificatechain")]
469 pub certificate_chain: String,
470 #[serde(rename = "privatekey")]
472 pub private_key: String,
473 #[serde(rename = "publickey")]
475 pub public_key: String,
476}
477
478#[derive(Serialize)]
479struct WithApiKeys<'a, T: Serialize> {
480 #[serde(flatten)]
481 api_key: &'a ApiKey,
482 #[serde(flatten)]
483 inner: T,
484}
485
486pub struct Client<P: MakeRequest> {
488 inner: P,
489 api_key: ApiKey,
490}
491
492#[cfg(feature = "default-client")]
493impl Client<DefaultTransport> {
494 pub fn new(api_key: ApiKey) -> Self {
498 Client::new_with_transport(api_key, DefaultTransport::default())
499 }
500}
501
502impl<T> Client<T>
503where
504 T: MakeRequest,
505 <T::Body as Body>::Error: Into<T::Error>,
506{
507 pub fn new_with_transport(api_key: ApiKey, transport: T) -> Self {
511 Self {
512 inner: transport,
513 api_key,
514 }
515 }
516 async fn post<D: for<'a> Deserialize<'a>>(
517 &self,
518 uri: Uri,
519 body: Full<Bytes>,
520 ) -> Result<D, Error<T::Error>> {
521 let request = Request::post(uri).body(body).unwrap(); let resp = self
523 .inner
524 .request(request)
525 .await
526 .map_err(ErrorImpl::TransportError)?;
527 let (head, body) = resp.into_parts();
528 let bytes = body
529 .collect()
530 .await
531 .map_err(|e| ErrorImpl::TransportError(e.into()))?
532 .to_bytes();
533 let result = std::result::Result::<_, ApiErrorMessage>::from(
534 serde_json::from_slice::<ApiResponse<_>>(&bytes)
535 .map_err(ErrorImpl::DeserializationError)?,
536 );
537
538 match (head.status, result) {
539 (StatusCode::OK, Ok(x)) => Ok(x),
540 (status, maybe_message) => Err((status, maybe_message.err()).into()),
541 }
542 }
543 async fn post_with_api_key<S: Serialize, D: for<'a> Deserialize<'a>>(
544 &self,
545 uri: Uri,
546 body: S,
547 ) -> Result<D, Error<T::Error>> {
548 let with_api_key = WithApiKeys {
549 api_key: &self.api_key,
550 inner: body,
551 };
552 let json = serde_json::to_string(&with_api_key).map_err(ErrorImpl::SerializationError)?;
553 let body = http_body_util::Full::new(Bytes::from(json));
554 self.post(uri, body).await
555 }
556
557 #[cfg_attr(feature = "tracing", tracing::instrument(skip(self)))]
559 pub async fn ping(&self) -> Result<IpAddr, Error<T::Error>> {
560 #[derive(Deserialize)]
561 #[serde(rename_all = "camelCase")]
562 struct PingResponse {
563 your_ip: IpAddr,
564 }
565 let ping: PingResponse = self.post_with_api_key(uri::ping(), ()).await?;
566 Ok(ping.your_ip)
567 }
568
569 #[cfg_attr(feature = "tracing", tracing::instrument(skip(self)))]
575 pub async fn domain_pricing(&self) -> Result<HashMap<String, Pricing>, Error<T::Error>> {
576 let resp: DomainPricingResponse = self.post(uri::domain_pricing(), Full::default()).await?;
577 Ok(resp.pricing)
578 }
579
580 #[cfg_attr(feature = "tracing", tracing::instrument(skip(self)))]
583 pub async fn icann_domain_pricing(
584 &self,
585 ) -> Result<impl Iterator<Item = (String, Pricing)>, Error<T::Error>> {
586 let resp: DomainPricingResponse = self.post(uri::domain_pricing(), Full::default()).await?;
587 Ok(resp.pricing.into_iter().filter(|(_, v)| v.is_icann()))
588 }
589
590 async fn list_domains(&self, offset: usize) -> Result<Vec<DomainInfo>, Error<T::Error>> {
591 let resp: DomainListAllResponse = self
592 .post_with_api_key(
593 uri::domain_list_all(),
594 DomainListAll {
595 start: offset,
596 include_labels: true,
597 },
598 )
599 .await?;
600 Ok(resp.domains)
601 }
602
603 #[cfg_attr(feature = "tracing", tracing::instrument(skip(self)))]
605 pub async fn domains(&self) -> Result<Vec<DomainInfo>, Error<T::Error>> {
606 let mut all = self.list_domains(0).await?;
607 let mut last_len = all.len();
608 while last_len != 0 {
610 let next = self.list_domains(all.len()).await?;
611 last_len = next.len();
612 all.extend(next.into_iter());
613 }
614 Ok(all)
615 }
616
617 #[cfg_attr(feature = "tracing", tracing::instrument(skip(self)))]
619 pub async fn get_all(&self, domain: &str) -> Result<Vec<DnsEntry>, Error<T::Error>> {
620 let rsp: DnsRecordsByDomainOrIDResponse = self
621 .post_with_api_key(uri::get_dns_record_by_domain_and_id(domain, None)?, ())
622 .await?;
623 Ok(rsp.records)
624 }
625
626 #[cfg_attr(feature = "tracing", tracing::instrument(skip(self)))]
628 pub async fn get_single(
629 &self,
630 domain: &str,
631 id: &str,
632 ) -> Result<Option<DnsEntry>, Error<T::Error>> {
633 let rsp: DnsRecordsByDomainOrIDResponse = self
634 .post_with_api_key(uri::get_dns_record_by_domain_and_id(domain, Some(id))?, ())
635 .await?;
636 let rsp = rsp.records.into_iter().next();
637 Ok(rsp)
638 }
639
640 #[cfg_attr(feature = "tracing", tracing::instrument(skip(self)))]
643 pub async fn create(
644 &self,
645 domain: &str,
646 cmd: CreateOrEditDnsRecord<'_>,
647 ) -> Result<String, Error<T::Error>> {
648 let resp: EntryId = self
649 .post_with_api_key(uri::create_dns_record(domain)?, cmd)
650 .await?;
651 Ok(resp.id)
652 }
653
654 #[cfg_attr(feature = "tracing", tracing::instrument(skip(self)))]
657 pub async fn edit(
658 &self,
659 domain: &str,
660 id: &str,
661 cmd: CreateOrEditDnsRecord<'_>,
662 ) -> Result<(), Error<T::Error>> {
663 self.post_with_api_key(uri::edit_dns_record(domain, id)?, cmd)
664 .await
665 }
666
667 #[cfg_attr(feature = "tracing", tracing::instrument(skip(self)))]
670 pub async fn delete(&self, domain: &str, id: &str) -> Result<(), Error<T::Error>> {
671 self.post_with_api_key(uri::delete_dns_record_by_id(domain, id)?, ())
672 .await
673 }
674
675 #[cfg_attr(feature = "tracing", tracing::instrument(skip(self)))]
677 pub async fn nameservers(&self, domain: &str) -> Result<Vec<String>, Error<T::Error>> {
678 let resp: UpdateNameServers = self
679 .post_with_api_key(uri::get_name_servers(domain)?, ())
680 .await?;
681 Ok(resp.ns)
682 }
683
684 #[cfg_attr(feature = "tracing", tracing::instrument(skip(self)))]
686 pub async fn update_nameservers(
687 &self,
688 domain: &str,
689 name_servers: Vec<String>,
690 ) -> Result<(), Error<T::Error>> {
691 self.post_with_api_key(
692 uri::update_name_servers(domain)?,
693 UpdateNameServers { ns: name_servers },
694 )
695 .await
696 }
697
698 #[cfg_attr(feature = "tracing", tracing::instrument(skip(self)))]
700 pub async fn get_url_forwards(
701 &self,
702 domain: &str,
703 ) -> Result<Vec<ForwardWithID>, Error<T::Error>> {
704 let resp: GetUrlForwardingResponse = self
705 .post_with_api_key(uri::get_url_forward(domain)?, ())
706 .await?;
707 Ok(resp.forwards)
708 }
709
710 #[cfg_attr(feature = "tracing", tracing::instrument(skip(self)))]
712 pub async fn add_url_forward(&self, domain: &str, cmd: Forward) -> Result<(), Error<T::Error>> {
713 self.post_with_api_key(uri::add_url_forward(domain)?, cmd)
714 .await
715 }
716
717 #[cfg_attr(feature = "tracing", tracing::instrument(skip(self)))]
719 pub async fn delete_url_forward(&self, domain: &str, id: &str) -> Result<(), Error<T::Error>> {
720 self.post_with_api_key(uri::delete_url_forward(domain, id)?, ())
721 .await
722 }
723
724 #[cfg_attr(feature = "tracing", tracing::instrument(skip(self)))]
726 pub async fn get_ssl_bundle(&mut self, domain: &str) -> Result<SslBundle, Error<T::Error>> {
727 self.post_with_api_key(uri::get_ssl_bundle(domain)?, ())
728 .await
729 }
730}