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.
"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),
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)
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,
-use crate::{utils::CidrAddress, SummaryOption};
+use crate::{
+ utils::{CidrAddress, Fqdn},
+ SummaryOption,
+};
use std::{
fmt,
net::{IpAddr, Ipv4Addr},
#[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,
// 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(),
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()),
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"
+ );
+ }
+}