]> git.proxmox.com Git - pve-installer.git/commitdiff
tui: introduce proper type for handling FQDNs
authorChristoph Heiss <c.heiss@proxmox.com>
Wed, 14 Jun 2023 07:37:54 +0000 (09:37 +0200)
committerChristoph Heiss <c.heiss@proxmox.com>
Wed, 14 Jun 2023 08:39:56 +0000 (10:39 +0200)
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
proxmox-tui-installer/src/main.rs
proxmox-tui-installer/src/options.rs
proxmox-tui-installer/src/utils.rs

index bcde2124f1b2cc5f37defb35d95d4fcde00f4078..856ea9b95a0db47a2c8de71c2c965deb61a54d68 100644 (file)
@@ -16,6 +16,7 @@ use cursive::{
     Cursive, View,
 };
 use std::net::IpAddr;
+use utils::Fqdn;
 use views::{BootdiskOptionsView, CidrAddressEditView, FormView, TableView, TableViewItem};
 
 // TextView::center() seems to garble the first two lines, so fix it manually here.
@@ -326,7 +327,10 @@ fn network_dialog(siv: &mut Cursive) -> InstallerView {
             "Management interface",
             SelectView::new().popup().with_all_str(vec!["eth0"]),
         )
-        .child("Hostname (FQDN)", EditView::new().content(options.fqdn))
+        .child(
+            "Hostname (FQDN)",
+            EditView::new().content(options.fqdn.to_string()),
+        )
         .child(
             "IP address (CIDR)",
             CidrAddressEditView::new().content(options.address),
@@ -351,7 +355,9 @@ fn network_dialog(siv: &mut Cursive) -> InstallerView {
 
                 let fqdn = view
                     .get_value::<EditView, _>(1)
-                    .ok_or("failed to retrieve host FQDN")?;
+                    .ok_or("failed to retrieve host FQDN")?
+                    .parse::<Fqdn>()
+                    .map_err(|_| "failed to parse hostname".to_owned())?;
 
                 let address = view
                     .get_value::<CidrAddressEditView, _>(2)
@@ -373,9 +379,11 @@ fn network_dialog(siv: &mut Cursive) -> InstallerView {
                     Err("host and gateway IP address version must not differ".to_owned())
                 } else if address.addr().is_ipv4() != dns_server.is_ipv4() {
                     Err("host and DNS IP address version must not differ".to_owned())
-                } else if fqdn.chars().all(|c| c.is_ascii_digit()) {
+                } else if fqdn.to_string().chars().all(|c| c.is_ascii_digit()) {
                     // Not supported/allowed on Debian
                     Err("hostname cannot be purely numeric".to_owned())
+                } else if fqdn.to_string().ends_with(".invalid") {
+                    Err("hostname does not look valid".to_owned())
                 } else {
                     Ok(NetworkOptions {
                         ifname,
index fdcbb51ce3d17f8ca2a83186b216405decd82456..8dbe675cf2b2a3241ae272c3d7b2f9ff9b2ccd5f 100644 (file)
@@ -1,4 +1,7 @@
-use crate::{utils::CidrAddress, SummaryOption};
+use crate::{
+    utils::{CidrAddress, Fqdn},
+    SummaryOption,
+};
 use std::{
     fmt,
     net::{IpAddr, Ipv4Addr},
@@ -282,7 +285,7 @@ impl Default for PasswordOptions {
 #[derive(Clone, Debug)]
 pub struct NetworkOptions {
     pub ifname: String,
-    pub fqdn: String,
+    pub fqdn: Fqdn,
     pub address: CidrAddress,
     pub gateway: IpAddr,
     pub dns_server: IpAddr,
@@ -293,7 +296,7 @@ impl Default for NetworkOptions {
         // TODO: Retrieve automatically
         Self {
             ifname: String::new(),
-            fqdn: "pve.example.invalid".to_owned(),
+            fqdn: "pve.example.invalid".parse().unwrap(),
             // Safety: The provided mask will always be valid.
             address: CidrAddress::new(Ipv4Addr::UNSPECIFIED, 0).unwrap(),
             gateway: Ipv4Addr::UNSPECIFIED.into(),
@@ -327,7 +330,7 @@ impl InstallerOptions {
             SummaryOption::new("Keyboard layout", &self.timezone.kb_layout),
             SummaryOption::new("Administator email", &self.password.email),
             SummaryOption::new("Management interface", &self.network.ifname),
-            SummaryOption::new("Hostname", &self.network.fqdn),
+            SummaryOption::new("Hostname", self.network.fqdn.to_string()),
             SummaryOption::new("Host IP (CIDR)", self.network.address.to_string()),
             SummaryOption::new("Gateway", self.network.gateway.to_string()),
             SummaryOption::new("DNS", self.network.dns_server.to_string()),
index fa974e972e69e77cc060521f41ab6b995cd31fc2..eed0bfbbd48911858451be7037df56b0ab8379ef 100644 (file)
@@ -97,3 +97,98 @@ fn mask_limit(addr: &IpAddr) -> usize {
         128
     }
 }
+
+#[derive(Clone, Debug)]
+pub struct Fqdn {
+    host: String,
+    domain: String,
+}
+
+impl Fqdn {
+    pub fn from(fqdn: &str) -> Result<Self, ()> {
+        let (host, domain) = fqdn.split_once('.').ok_or(())?;
+
+        if !Self::validate_single(host) || !domain.split('.').all(Self::validate_single) {
+            Err(())
+        } else {
+            Ok(Self {
+                host: host.to_owned(),
+                domain: domain.to_owned(),
+            })
+        }
+    }
+
+    #[cfg(test)]
+    pub fn host(&self) -> &str {
+        &self.host
+    }
+
+    #[cfg(test)]
+    pub fn domain(&self) -> &str {
+        &self.domain
+    }
+
+    fn validate_single(s: &str) -> bool {
+        !s.is_empty()
+            && s.chars()
+                .next()
+                .map(|c| c.is_ascii_alphanumeric())
+                .unwrap_or_default()
+            && s.chars()
+                .last()
+                .map(|c| c.is_ascii_alphanumeric())
+                .unwrap_or_default()
+            && s.chars()
+                .skip(1)
+                .take(s.len().saturating_sub(2))
+                .all(|c| c.is_ascii_alphanumeric() || c == '-')
+    }
+}
+
+impl FromStr for Fqdn {
+    type Err = ();
+
+    fn from_str(value: &str) -> Result<Self, Self::Err> {
+        Self::from(value)
+    }
+}
+
+impl fmt::Display for Fqdn {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        write!(f, "{}.{}", self.host, self.domain)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn fqdn_validate_single() {
+        assert!(Fqdn::from("foo.example.com").is_ok());
+        assert!(Fqdn::from("1.example.com").is_ok());
+        assert!(Fqdn::from("foo-bar.com").is_ok());
+        assert!(Fqdn::from("a-b.com").is_ok());
+
+        assert!(Fqdn::from("foo").is_err());
+        assert!(Fqdn::from("-foo.com").is_err());
+        assert!(Fqdn::from("foo-.com").is_err());
+        assert!(Fqdn::from("foo.com-").is_err());
+        assert!(Fqdn::from("-o-.com").is_err());
+    }
+
+    #[test]
+    fn fqdn_parts() {
+        let fqdn = Fqdn::from("pve.example.com").unwrap();
+        assert_eq!(fqdn.host(), "pve");
+        assert_eq!(fqdn.domain(), "example.com");
+    }
+
+    #[test]
+    fn fqdn_display() {
+        assert_eq!(
+            Fqdn::from("foo.example.com").unwrap().to_string(),
+            "foo.example.com"
+        );
+    }
+}