1#![warn(missing_docs)]
2mod serde_util;
42
43pub mod transport;
44#[cfg(feature = "default-client")]
45use transport::DefaultTransport;
46use transport::MakeRequest;
47mod error;
48pub use error::Error;
49use error::{ApiErrorMessage, ApiResponse, ErrorImpl};
50mod uri;
51
52use chrono::NaiveDateTime;
53use http_body_util::{BodyExt, Full};
54use hyper::{
55 body::{Body, Bytes},
56 Request, StatusCode, Uri,
57};
58use serde::{Deserialize, Serialize};
59use std::{
60 borrow::Cow,
61 collections::HashMap,
62 fmt::Display,
63 net::{IpAddr, Ipv4Addr, Ipv6Addr},
64 time::Duration,
65};
66
67#[derive(Deserialize, Serialize, Clone)]
69pub struct ApiKey {
70 secretapikey: String,
71 apikey: String,
72}
73
74impl ApiKey {
75 pub fn new(secret: impl Into<String>, api_key: impl Into<String>) -> Self {
77 Self {
78 secretapikey: secret.into(),
79 apikey: api_key.into(),
80 }
81 }
82}
83
84#[allow(missing_docs)]
86#[derive(Clone, Copy, Deserialize, Serialize, Debug, PartialEq, Eq)]
87pub enum DnsRecordType {
88 A,
89 MX,
90 CNAME,
91 ALIAS,
92 TXT,
93 NS,
94 AAAA,
95 SRV,
96 TLSA,
97 CAA,
98 HTTPS,
99 SVCB,
100}
101
102impl Display for DnsRecordType {
103 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104 f.write_str(match self {
105 Self::A => "A",
106 Self::MX => "MX",
107 Self::CNAME => "CNAME",
108 Self::ALIAS => "ALIAS",
109 Self::TXT => "TXT",
110 Self::NS => "NS",
111 Self::AAAA => "AAAA",
112 Self::SRV => "SRV",
113 Self::TLSA => "TLSA",
114 Self::CAA => "CAA",
115 Self::HTTPS => "HTTPS",
116 Self::SVCB => "SVCB",
117 })
118 }
119}
120
121#[derive(Debug, Serialize, PartialEq, Eq)]
123pub struct CreateOrEditDnsRecord<'a> {
124 #[serde(rename = "name")]
126 pub subdomain: Option<&'a str>,
127 #[serde(rename = "type")]
129 pub record_type: DnsRecordType,
130 pub content: Cow<'a, str>,
132 pub ttl: Option<u64>,
134 pub prio: u32,
137 }
141
142impl<'a> CreateOrEditDnsRecord<'a> {
143 pub fn new(
145 subdomain: Option<&'a str>,
146 record_type: DnsRecordType,
147 content: impl Into<Cow<'a, str>>,
148 ) -> Self {
149 Self {
150 subdomain,
151 record_type,
152 content: content.into(),
153 ttl: None,
154 prio: 0,
155 }
156 }
157 #[allow(non_snake_case)]
159 pub fn A(subdomain: Option<&'a str>, ip: Ipv4Addr) -> Self {
160 Self::new(subdomain, DnsRecordType::A, Cow::Owned(ip.to_string()))
161 }
162 #[allow(non_snake_case)]
164 pub fn AAAA(subdomain: Option<&'a str>, ip: Ipv6Addr) -> Self {
165 Self::new(subdomain, DnsRecordType::AAAA, Cow::Owned(ip.to_string()))
166 }
167 #[allow(non_snake_case)]
169 pub fn A_or_AAAA(subdomain: Option<&'a str>, ip: IpAddr) -> Self {
170 match ip {
171 IpAddr::V4(my_ip) => Self::A(subdomain, my_ip),
172 IpAddr::V6(my_ip) => Self::AAAA(subdomain, my_ip),
173 }
174 }
175
176 #[must_use]
179 pub fn with_ttl(self, ttl: Option<Duration>) -> Self {
180 Self {
181 ttl: ttl.as_ref().map(Duration::as_secs),
182 ..self
183 }
184 }
185
186 #[must_use]
188 pub fn with_priority(self, prio: u32) -> Self {
189 Self { prio, ..self }
190 }
191}
192
193#[derive(Deserialize, Debug)]
198struct EntryId {
199 #[serde(with = "serde_util::string_or_int")]
200 id: String,
201}
202
203#[derive(Deserialize, Debug)]
205pub struct DnsEntry {
206 #[serde(with = "serde_util::string_or_int")]
208 pub id: String,
209 pub name: String,
211 #[serde(rename = "type")]
213 pub record_type: DnsRecordType,
214 pub content: String,
216 #[serde(with = "serde_util::u64_from_string_or_int")]
219 pub ttl: u64,
220 #[serde(default, with = "serde_util::u64_from_string_or_int")]
223 pub prio: u64,
224 pub notes: Option<String>,
227}
228
229#[derive(Deserialize, Debug)]
230struct DnsRecordsByDomainOrIDResponse {
231 records: Vec<DnsEntry>,
232}
233
234#[derive(Deserialize, Debug)]
236#[serde(rename_all = "camelCase")]
237pub struct Pricing {
238 pub registration: String,
240 pub renewal: String,
242 pub transfer: String,
244 pub special_type: TldType,
251}
252
253impl Pricing {
254 pub fn is_icann(&self) -> bool {
256 self.special_type.is_icann()
257 }
258}
259
260#[derive(Debug)]
262pub enum TldType {
263 Normal,
266 Handshake,
268 Other(String),
272}
273
274impl TldType {
275 pub fn is_icann(&self) -> bool {
277 matches!(self, Self::Normal)
278 }
279}
280
281impl<'de> Deserialize<'de> for TldType {
282 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
283 where
284 D: serde::Deserializer<'de>,
285 {
286 let string_value = Option::<String>::deserialize(deserializer)?;
287 Ok(match string_value {
288 None => Self::Normal,
289 Some(s) => {
290 if s.eq_ignore_ascii_case("handshake") {
291 Self::Handshake
292 } else {
293 Self::Other(s)
294 }
295 }
296 })
297 }
298}
299
300#[derive(Deserialize, Debug)]
301struct DomainPricingResponse {
302 pricing: HashMap<String, Pricing>,
304}
305
306#[derive(Serialize, Deserialize)]
307struct UpdateNameServers {
308 ns: Vec<String>,
309}
310
311#[derive(Serialize)]
312#[serde(rename_all = "camelCase")]
313struct DomainListAll {
314 start: usize,
316 #[serde(default, with = "serde_util::yesno")]
318 include_labels: bool,
319}
320
321#[derive(Deserialize, Debug)]
322#[serde(rename_all = "camelCase")]
323struct DomainListAllResponse {
324 domains: Vec<DomainInfo>,
325}
326
327#[derive(Deserialize, Debug)]
329#[serde(rename_all = "camelCase")]
330pub struct DomainInfo {
331 pub domain: String,
333 pub status: String,
336 pub tld: String,
338 #[serde(with = "serde_util::datetime")]
341 pub create_date: NaiveDateTime,
342 #[serde(with = "serde_util::datetime")]
344 pub expire_date: NaiveDateTime,
345 #[serde(with = "serde_util::stringoneintzero")]
348 pub security_lock: bool,
349 #[serde(with = "serde_util::stringoneintzero")]
350 pub whois_privacy: bool,
352 #[serde(with = "serde_util::stringoneintzero")]
355 pub auto_renew: bool,
356 #[serde(with = "serde_util::stringoneintzero")]
358 pub not_local: bool,
359 #[serde(default)]
361 pub labels: Vec<Label>,
362}
363
364#[derive(Deserialize, Debug)]
366#[serde(rename_all = "camelCase")]
367pub struct Label {
368 #[serde(deserialize_with = "serde_util::string_or_int::deserialize")]
370 pub id: String,
371 pub title: String,
373 pub color: String,
375}
376
377#[derive(Serialize, Deserialize, Debug)]
379#[serde(rename_all = "camelCase")]
380pub struct Forward {
381 #[serde(skip_serializing_if = "Option::is_none")]
383 pub subdomain: Option<String>,
384 pub location: String,
386 #[serde(rename = "type")]
387 pub forward_type: ForwardType,
389 #[serde(with = "serde_util::yesno")]
391 pub include_path: bool,
392 #[serde(with = "serde_util::yesno")]
394 pub wildcard: bool,
395}
396
397impl Forward {
398 pub fn new(subdomain: Option<impl Into<String>>, to: impl Into<String>) -> Self {
401 Self {
402 subdomain: subdomain.map(|s| s.into()),
403 location: to.into(),
404 forward_type: ForwardType::Temporary,
405 include_path: true,
406 wildcard: false,
407 }
408 }
409
410 pub fn with_wildcard(self, wildcard: bool) -> Self {
412 Self { wildcard, ..self }
413 }
414
415 pub fn include_path(self, include_path: bool) -> Self {
417 Self {
418 include_path,
419 ..self
420 }
421 }
422
423 pub fn with_forward_type(self, forward_type: ForwardType) -> Self {
425 Self {
426 forward_type,
427 ..self
428 }
429 }
430}
431
432#[allow(missing_docs)]
434#[derive(Deserialize, Serialize, Debug)]
435#[serde(rename_all = "lowercase")]
436pub enum ForwardType {
437 Temporary,
438 Permanent,
439}
440
441#[derive(Deserialize, Debug)]
442#[serde(rename_all = "camelCase")]
443struct GetUrlForwardingResponse {
444 forwards: Vec<ForwardWithID>,
445}
446
447#[derive(Deserialize, Debug)]
449#[serde(rename_all = "camelCase")]
450pub struct ForwardWithID {
451 #[serde(deserialize_with = "serde_util::string_or_int::deserialize")]
453 pub id: String,
454 #[serde(flatten, rename = "forward")]
456 pub config: Forward,
457}
458
459#[derive(Deserialize, Debug)]
461pub struct SslBundle {
462 #[serde(rename = "certificatechain")]
464 pub certificate_chain: String,
465 #[serde(rename = "privatekey")]
467 pub private_key: String,
468 #[serde(rename = "publickey")]
470 pub public_key: String,
471}
472
473#[derive(Serialize)]
474struct WithApiKeys<'a, T: Serialize> {
475 #[serde(flatten)]
476 api_key: &'a ApiKey,
477 #[serde(flatten)]
478 inner: T,
479}
480
481pub struct Client<P: MakeRequest> {
483 inner: P,
484 api_key: ApiKey,
485}
486
487#[cfg(feature = "default-client")]
488impl Client<DefaultTransport> {
489 pub fn new(api_key: ApiKey) -> Self {
493 Client::new_with_transport(api_key, DefaultTransport::default())
494 }
495}
496
497impl<T> Client<T>
498where
499 T: MakeRequest,
500 <T::Body as Body>::Error: Into<T::Error>,
501{
502 pub fn new_with_transport(api_key: ApiKey, transport: T) -> Self {
506 Self {
507 inner: transport,
508 api_key,
509 }
510 }
511 async fn post<D: for<'a> Deserialize<'a>>(
512 &self,
513 uri: Uri,
514 body: Full<Bytes>,
515 ) -> Result<D, Error<T::Error>> {
516 let request = Request::post(uri).body(body).unwrap(); let resp = self
518 .inner
519 .request(request)
520 .await
521 .map_err(ErrorImpl::TransportError)?;
522 let (head, body) = resp.into_parts();
523 let bytes = body
524 .collect()
525 .await
526 .map_err(|e| ErrorImpl::TransportError(e.into()))?
527 .to_bytes();
528 let result = std::result::Result::<_, ApiErrorMessage>::from(
529 serde_json::from_slice::<ApiResponse<_>>(&bytes)
530 .map_err(ErrorImpl::DeserializationError)?,
531 );
532
533 match (head.status, result) {
534 (StatusCode::OK, Ok(x)) => Ok(x),
535 (status, maybe_message) => Err((status, maybe_message.err()).into()),
536 }
537 }
538 async fn post_with_api_key<S: Serialize, D: for<'a> Deserialize<'a>>(
539 &self,
540 uri: Uri,
541 body: S,
542 ) -> Result<D, Error<T::Error>> {
543 let with_api_key = WithApiKeys {
544 api_key: &self.api_key,
545 inner: body,
546 };
547 let json = serde_json::to_string(&with_api_key).map_err(ErrorImpl::SerializationError)?;
548 let body = http_body_util::Full::new(Bytes::from(json));
549 self.post(uri, body).await
550 }
551
552 pub async fn ping(&self) -> Result<IpAddr, Error<T::Error>> {
554 #[derive(Deserialize)]
555 #[serde(rename_all = "camelCase")]
556 struct PingResponse {
557 your_ip: IpAddr,
558 }
559 let ping: PingResponse = self.post_with_api_key(uri::ping(), ()).await?;
560 Ok(ping.your_ip)
561 }
562
563 pub async fn domain_pricing(&self) -> Result<HashMap<String, Pricing>, Error<T::Error>> {
569 let resp: DomainPricingResponse = self.post(uri::domain_pricing(), Full::default()).await?;
570 Ok(resp.pricing)
571 }
572
573 pub async fn icann_domain_pricing(
576 &self,
577 ) -> Result<impl Iterator<Item = (String, Pricing)>, Error<T::Error>> {
578 let resp: DomainPricingResponse = self.post(uri::domain_pricing(), Full::default()).await?;
579 Ok(resp.pricing.into_iter().filter(|(_, v)| v.is_icann()))
580 }
581
582 async fn list_domains(&self, offset: usize) -> Result<Vec<DomainInfo>, Error<T::Error>> {
583 let resp: DomainListAllResponse = self
584 .post_with_api_key(
585 uri::domain_list_all(),
586 DomainListAll {
587 start: offset,
588 include_labels: true,
589 },
590 )
591 .await?;
592 Ok(resp.domains)
593 }
594
595 pub async fn domains(&self) -> Result<Vec<DomainInfo>, Error<T::Error>> {
597 let mut all = self.list_domains(0).await?;
598 let mut last_len = all.len();
599 while last_len != 0 {
601 let next = self.list_domains(all.len()).await?;
602 last_len = next.len();
603 all.extend(next.into_iter());
604 }
605 Ok(all)
606 }
607
608 pub async fn get_all(&self, domain: &str) -> Result<Vec<DnsEntry>, Error<T::Error>> {
610 let rsp: DnsRecordsByDomainOrIDResponse = self
611 .post_with_api_key(uri::get_dns_record_by_domain_and_id(domain, None)?, ())
612 .await?;
613 Ok(rsp.records)
614 }
615
616 pub async fn get_single(
618 &self,
619 domain: &str,
620 id: &str,
621 ) -> Result<Option<DnsEntry>, Error<T::Error>> {
622 let rsp: DnsRecordsByDomainOrIDResponse = self
623 .post_with_api_key(uri::get_dns_record_by_domain_and_id(domain, Some(id))?, ())
624 .await?;
625 let rsp = rsp.records.into_iter().next();
626 Ok(rsp)
627 }
628
629 pub async fn create(
632 &self,
633 domain: &str,
634 cmd: CreateOrEditDnsRecord<'_>,
635 ) -> Result<String, Error<T::Error>> {
636 let resp: EntryId = self
637 .post_with_api_key(uri::create_dns_record(domain)?, cmd)
638 .await?;
639 Ok(resp.id)
640 }
641
642 pub async fn edit(
645 &self,
646 domain: &str,
647 id: &str,
648 cmd: CreateOrEditDnsRecord<'_>,
649 ) -> Result<(), Error<T::Error>> {
650 self.post_with_api_key(uri::edit_dns_record(domain, id)?, cmd)
651 .await
652 }
653
654 pub async fn delete(&self, domain: &str, id: &str) -> Result<(), Error<T::Error>> {
657 self.post_with_api_key(uri::delete_dns_record_by_id(domain, id)?, ())
658 .await
659 }
660
661 pub async fn nameservers(&self, domain: &str) -> Result<Vec<String>, Error<T::Error>> {
663 let resp: UpdateNameServers = self
664 .post_with_api_key(uri::get_name_servers(domain)?, ())
665 .await?;
666 Ok(resp.ns)
667 }
668
669 pub async fn update_nameservers(
671 &self,
672 domain: &str,
673 name_servers: Vec<String>,
674 ) -> Result<(), Error<T::Error>> {
675 self.post_with_api_key(
676 uri::update_name_servers(domain)?,
677 UpdateNameServers { ns: name_servers },
678 )
679 .await
680 }
681
682 pub async fn get_url_forwards(
684 &self,
685 domain: &str,
686 ) -> Result<Vec<ForwardWithID>, Error<T::Error>> {
687 let resp: GetUrlForwardingResponse = self
688 .post_with_api_key(uri::get_url_forward(domain)?, ())
689 .await?;
690 Ok(resp.forwards)
691 }
692
693 pub async fn add_url_forward(&self, domain: &str, cmd: Forward) -> Result<(), Error<T::Error>> {
695 self.post_with_api_key(uri::add_url_forward(domain)?, cmd)
696 .await
697 }
698
699 pub async fn delete_url_forward(&self, domain: &str, id: &str) -> Result<(), Error<T::Error>> {
701 self.post_with_api_key(uri::delete_url_forward(domain, id)?, ())
702 .await
703 }
704
705 pub async fn get_ssl_bundle(&mut self, domain: &str) -> Result<SslBundle, Error<T::Error>> {
707 self.post_with_api_key(uri::get_ssl_bundle(domain)?, ())
708 .await
709 }
710}