Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_net/src/dns.rs

4.9 KiB, 64 runs

created by r1870400018:561, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1use crate::constant;
2
3use oxedyne_fe2o3_core::prelude::*;
4
5use std::net::ToSocketAddrs;
6
7
8new_type!(Fqdn, String, Clone, Debug, Default, PartialEq);
9
10/// Fully Qualified Domain Name
11impl Fqdn {
12
13 pub fn new<S: Into<String>>(fqdn: S) -> Outcome<Self> {
14 let fqdn = fqdn.into();
15 match Self::validate(&fqdn) {
16 Ok(()) => Ok(Self(fqdn)),
17 Err(e) => Err(e),
18 }
19 }
20
21 pub fn as_str(&self) -> &str { &self.0 }
22 pub fn to_string(&self) -> String { self.0.clone() }
23
24 /// Validates a Fully Qualified Domain Name (FQDN) according to:
25 /// - RFC 1035 Domain Name Implementation and Specification (base DNS rules)
26 /// - RFC 1123 Internet Host Requirements (allowing digits in first character)
27 /// - RFC 6761 Special-Use Domain Names (localhost etc.)
28 ///
29 /// Implements the "preferred name syntax" from RFC 952/1123 which requires:
30 /// - Labels separated by dots, 2-63 bytes each
31 /// - Labels use only ASCII alphanumeric chars and hyphens
32 /// - Labels don't start or end with hyphens
33 /// - Total name length ≤ 255 bytes
34 /// - At least two labels (except special cases from RFC 6761)
35 pub fn validate(fqdn: &str) -> Outcome<()> {
36
37 if fqdn.is_empty() {
38 return Err(err!(
39 "'{}': Domain name cannot be empty.", fqdn;
40 Invalid, Input, Size));
41 }
42
43 // Check for leading dots.
44 if fqdn.starts_with('.') {
45 return Err(err!(
46 "'{}': Domain name must not begin with dots.", fqdn;
47 Invalid, Input, String));
48 }
49
50 // Handle trailing dots - only single trailing dot is valid.
51 if fqdn.ends_with("..") {
52 return Err(err!(
53 "'{}': Domain name must not end with multiple dots.", fqdn;
54 Invalid, Input, String));
55 }
56
57 // Check total length - RFC 1035 limits domain names to 255 bytes.
58 if fqdn.len() > 255 {
59 return Err(err!(
60 "'{}': Domain name length of {} exceeds 255 bytes.", fqdn, fqdn.len();
61 Invalid, Input, Size));
62 }
63
64 // Remove single trailing dot if present for remaining validation
65 let name = fqdn.strip_suffix('.').unwrap_or(fqdn);
66
67 // Check for special cases first.
68 if constant::SPECIAL_DOMAINS.contains(&name) {
69 return Ok(());
70 }
71
72 // Split the FQDN into domain labels.
73 let labels: Vec<&str> = name.split('.').collect();
74
75 // Check if the FQDN has at least two labels (e.g., "example.com").
76 if labels.len() < 2 {
77 return Err(err!(
78 "'{}': Domain name must have at least one '.'.", fqdn;
79 Invalid, Input));
80 }
81
82 // Validate each domain label.
83 for label in labels {
84
85 // Check if the label is empty (consecutive dots).
86 if label.is_empty() {
87 return Err(err!(
88 "'{}': Empty domain label.", label;
89 Invalid, Input, Missing));
90 }
91
92 // Check if the label exceeds the maximum length (63 bytes max).
93 if label.len() > 63 {
94 return Err(err!(
95 "'{}': Domain name label length of {} exceeds 63.", label, label.len();
96 Invalid, Input, Size));
97 }
98
99 // Get first and last chars - we've already checked it's not empty.
100 let mut chars = label.chars();
101 let first_char = match chars.next() {
102 Some(c) => c,
103 None => return Err(err!(
104 "'{}': Invalid domain label.", label;
105 Invalid, Input, String))
106 };
107
108 let last_char = match chars.last() {
109 Some(c) => c,
110 None => first_char
111 };
112
113 // Check first character is letter or digit.
114 if !first_char.is_ascii_alphanumeric() {
115 return Err(err!(
116 "'{}': Domain name label must begin with a letter or digit.", label;
117 Invalid, Input, String));
118 }
119
120 // Check last character is letter or digit.
121 if !last_char.is_ascii_alphanumeric() {
122 return Err(err!(
123 "'{}': Domain name label must end with a letter or digit.", label;
124 Invalid, Input, String));
125 }
126
127 // Check all characters.
128 if !label.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') {
129 return Err(err!(
130 "'{}': Domain name label can only contain ASCII alphanumeric \
131 or '-' characters.", label;
132 Invalid, Input, String));
133 }
134 }
135
136 Ok(())
137 }
138
139 /// Attempt to resolve the FQDN to an IP address.
140 pub fn exists(&self) -> bool {
141 match self.to_socket_addrs() {
142 Ok(mut addrs) => addrs.next().is_some(),
143 Err(_) => false,
144 }
145 }
146}
147