Skip to main content

dinio_sdk/models/
redirect.rs

1use std::fmt;
2use std::str::FromStr;
3
4use serde::{Deserialize, Serialize};
5use uuid::Uuid;
6
7use crate::error::{Error, Result};
8
9/// Status codes Core accepts for a redirect.
10pub const REDIRECT_CODES: [u16; 5] = [301, 302, 303, 307, 308];
11
12/// An HTTP(S) redirect that Relay answers for a host name inside one of the caller's
13/// zones.
14///
15/// Core keeps the DNS records that point the name at the platform (a CNAME below the
16/// apex, A/AAAA at the apex, [`RecordManager::Redirect`](crate::models::RecordManager))
17/// and removes them with the redirect.
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct Redirect {
20    /// Host name the redirect answers for: lowercase, without trailing dot. Unique across
21    /// the platform; this is how a redirect is addressed.
22    pub name: String,
23    /// Zone the host name belongs to.
24    pub zone_id: Uuid,
25    /// HTTP status code, one of [`REDIRECT_CODES`] today.
26    pub code: u16,
27    /// Target `http://` or `https://` URL.
28    pub location: String,
29    /// `Auto`, `Manual` or `None` (kept as a string for forward compatibility); see
30    /// [`RedirectTls`] and [`Redirect::tls_mode`].
31    pub tls: String,
32    /// Unix seconds.
33    pub created_at: i64,
34    /// Unix seconds.
35    pub updated_at: i64,
36    /// State of the automatic certificate; only for `tls` = `Auto`.
37    #[serde(default, skip_serializing_if = "Option::is_none")]
38    pub certificate: Option<RedirectCertificate>,
39}
40
41impl Redirect {
42    /// `tls` in its typed form, if this SDK version knows it.
43    pub fn tls_mode(&self) -> Option<RedirectTls> {
44        self.tls.parse().ok()
45    }
46}
47
48/// Where the HTTPS certificate of a redirect comes from (`Redirect.tls`).
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
50pub enum RedirectTls {
51    /// Core issues and renews a Let's Encrypt certificate for the name.
52    Auto,
53    /// The user uploads their own certificate
54    /// ([`Certificates::create`](crate::api::Certificates::create)).
55    Manual,
56    /// No certificate of its own; HTTPS requests get the platform's default certificate.
57    None,
58}
59
60impl RedirectTls {
61    /// The label Core uses (`Auto`, `Manual`, `None`).
62    pub fn as_wire(self) -> &'static str {
63        match self {
64            Self::Auto => "Auto",
65            Self::Manual => "Manual",
66            Self::None => "None",
67        }
68    }
69}
70
71impl fmt::Display for RedirectTls {
72    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73        f.write_str(self.as_wire())
74    }
75}
76
77impl FromStr for RedirectTls {
78    type Err = Error;
79
80    /// Case-insensitive wire label (`auto`, `Manual`, `NONE`).
81    fn from_str(text: &str) -> Result<Self> {
82        match text.trim().to_ascii_lowercase().as_str() {
83            "auto" => Ok(Self::Auto),
84            "manual" => Ok(Self::Manual),
85            "none" => Ok(Self::None),
86            _ => Err(Error::InvalidInput(format!(
87                "unknown redirect tls {text:?} (Auto, Manual or None)"
88            ))),
89        }
90    }
91}
92
93/// The automatic certificate of a redirect with `tls` = `Auto`.
94#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct RedirectCertificate {
96    /// `Pending`, `Valid` or `Failed` (kept as a string for forward compatibility); see
97    /// [`AcmeStatus`] and [`RedirectCertificate::kind`].
98    pub status: String,
99    /// The issued certificate, once there is one.
100    #[serde(default, skip_serializing_if = "Option::is_none")]
101    pub certificate_id: Option<Uuid>,
102    /// Unix seconds: when Core next works on this certificate (retry or renewal).
103    pub renew_at: i64,
104    /// Why the last attempt failed.
105    #[serde(default, skip_serializing_if = "Option::is_none")]
106    pub last_error: Option<String>,
107}
108
109impl RedirectCertificate {
110    /// `status` in its typed form, if this SDK version knows it.
111    pub fn kind(&self) -> Option<AcmeStatus> {
112        self.status.parse().ok()
113    }
114}
115
116/// State of an automatically issued certificate (`RedirectCertificate.status`).
117#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
118pub enum AcmeStatus {
119    /// Being issued; usually takes minutes.
120    Pending,
121    /// Issued and in use.
122    Valid,
123    /// The last attempt failed (see `last_error`); Core retries on its own.
124    Failed,
125}
126
127impl FromStr for AcmeStatus {
128    type Err = Error;
129
130    /// Case-insensitive wire label (`pending`, `Valid`).
131    fn from_str(text: &str) -> Result<Self> {
132        match text.trim().to_ascii_lowercase().as_str() {
133            "pending" => Ok(Self::Pending),
134            "valid" => Ok(Self::Valid),
135            "failed" => Ok(Self::Failed),
136            _ => Err(Error::InvalidInput(format!(
137                "unknown certificate status {text:?} (Pending, Valid or Failed)"
138            ))),
139        }
140    }
141}
142
143/// `POST /redirects` body.
144#[derive(Debug, Clone, Serialize)]
145pub struct NewRedirect {
146    /// Zone the host name belongs to; the caller needs access to it. See
147    /// [`ops::find_zone_for_host`](crate::ops::find_zone_for_host).
148    pub zone_id: Uuid,
149    /// Host name inside the zone, e.g. `go.example.com` (case and a trailing dot do not
150    /// matter). It must not have A, AAAA or CNAME records of the user's own.
151    pub name: String,
152    /// One of [`REDIRECT_CODES`]. 301 and 308 are permanent: browsers may cache them for a
153    /// long time.
154    pub code: u16,
155    /// `http://` or `https://` URL, up to 2048 characters.
156    pub location: String,
157    /// Where the HTTPS certificate comes from; Core uses [`RedirectTls::None`] when this
158    /// is `None`.
159    #[serde(skip_serializing_if = "Option::is_none")]
160    pub tls: Option<RedirectTls>,
161}
162
163/// `PATCH /redirects/:name` body; `None` fields stay unchanged. The name (and so the
164/// zone) cannot change.
165#[derive(Debug, Clone, Default, Serialize)]
166pub struct RedirectPatch {
167    /// One of [`REDIRECT_CODES`].
168    #[serde(skip_serializing_if = "Option::is_none")]
169    pub code: Option<u16>,
170    /// `http://` or `https://` URL, up to 2048 characters.
171    #[serde(skip_serializing_if = "Option::is_none")]
172    pub location: Option<String>,
173    /// New certificate source. Switching to `Auto` starts issuance.
174    #[serde(skip_serializing_if = "Option::is_none")]
175    pub tls: Option<RedirectTls>,
176}
177
178impl RedirectPatch {
179    /// Whether the patch changes nothing.
180    pub fn is_empty(&self) -> bool {
181        self.code.is_none() && self.location.is_none() && self.tls.is_none()
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188
189    #[test]
190    fn redirect_with_certificate_deserializes() {
191        let redirect: Redirect = serde_json::from_value(serde_json::json!({
192            "name": "go.example.com", "zone_id": "00000000-0000-0000-0000-000000000001",
193            "code": 302, "location": "https://example.com/", "tls": "Auto",
194            "created_at": 1, "updated_at": 2,
195            "certificate": { "status": "Pending", "renew_at": 3 }
196        }))
197        .unwrap();
198        assert_eq!(redirect.tls_mode(), Some(RedirectTls::Auto));
199        let cert = redirect.certificate.unwrap();
200        assert_eq!(cert.kind(), Some(AcmeStatus::Pending));
201        assert_eq!(cert.certificate_id, None);
202    }
203
204    #[test]
205    fn unknown_tls_is_kept() {
206        let redirect: Redirect = serde_json::from_value(serde_json::json!({
207            "name": "go.example.com", "zone_id": "00000000-0000-0000-0000-000000000001",
208            "code": 302, "location": "https://example.com/", "tls": "Something",
209            "created_at": 1, "updated_at": 2
210        }))
211        .unwrap();
212        assert_eq!(redirect.tls, "Something");
213        assert_eq!(redirect.tls_mode(), None);
214    }
215
216    #[test]
217    fn new_redirect_wire_shape() {
218        let new = NewRedirect {
219            zone_id: Uuid::nil(),
220            name: "go.example.com".into(),
221            code: 302,
222            location: "https://example.com/".into(),
223            tls: Some(RedirectTls::None),
224        };
225        assert_eq!(
226            serde_json::to_value(&new).unwrap(),
227            serde_json::json!({
228                "zone_id": "00000000-0000-0000-0000-000000000000",
229                "name": "go.example.com", "code": 302,
230                "location": "https://example.com/", "tls": "None",
231            })
232        );
233    }
234}