rustls/
dns_name.rs

1/// DNS name validation according to RFC1035, but with underscores allowed.
2use std::error::Error as StdError;
3use std::fmt;
4
5/// A type which encapsulates an owned string that is a syntactically valid DNS name.
6#[derive(Clone, Eq, Hash, PartialEq, Debug)]
7pub struct DnsName(String);
8
9impl<'a> DnsName {
10    /// Produce a borrowed `DnsNameRef` from this owned `DnsName`.
11    pub fn borrow(&'a self) -> DnsNameRef<'a> {
12        DnsNameRef(self.as_ref())
13    }
14
15    /// Validate the given bytes are a DNS name if they are viewed as ASCII.
16    pub fn try_from_ascii(bytes: &[u8]) -> Result<Self, InvalidDnsNameError> {
17        // nb. a sequence of bytes that is accepted by `validate()` is both
18        // valid UTF-8, and valid ASCII.
19        String::from_utf8(bytes.to_vec())
20            .map_err(|_| InvalidDnsNameError)
21            .and_then(Self::try_from)
22    }
23}
24
25impl TryFrom<String> for DnsName {
26    type Error = InvalidDnsNameError;
27
28    fn try_from(value: String) -> Result<Self, Self::Error> {
29        validate(value.as_bytes())?;
30        Ok(Self(value))
31    }
32}
33
34impl AsRef<str> for DnsName {
35    fn as_ref(&self) -> &str {
36        AsRef::<str>::as_ref(&self.0)
37    }
38}
39
40/// A type which encapsulates a borrowed string that is a syntactically valid DNS name.
41#[derive(Eq, Hash, PartialEq, Debug)]
42pub struct DnsNameRef<'a>(&'a str);
43
44impl<'a> DnsNameRef<'a> {
45    /// Copy this object to produce an owned `DnsName`.
46    pub fn to_owned(&'a self) -> DnsName {
47        DnsName(self.0.to_string())
48    }
49
50    /// Copy this object to produce an owned `DnsName`, smashing the case to lowercase
51    /// in one operation.
52    pub fn to_lowercase_owned(&'a self) -> DnsName {
53        DnsName(self.0.to_lowercase())
54    }
55}
56
57impl<'a> TryFrom<&'a str> for DnsNameRef<'a> {
58    type Error = InvalidDnsNameError;
59
60    fn try_from(value: &'a str) -> Result<Self, Self::Error> {
61        validate(value.as_bytes())?;
62        Ok(DnsNameRef(value))
63    }
64}
65
66impl<'a> AsRef<str> for DnsNameRef<'a> {
67    fn as_ref(&self) -> &str {
68        self.0
69    }
70}
71
72/// The provided input could not be parsed because
73/// it is not a syntactically-valid DNS Name.
74#[derive(Debug)]
75pub struct InvalidDnsNameError;
76
77impl fmt::Display for InvalidDnsNameError {
78    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
79        f.write_str("invalid dns name")
80    }
81}
82
83impl StdError for InvalidDnsNameError {}
84
85fn validate(input: &[u8]) -> Result<(), InvalidDnsNameError> {
86    use State::*;
87    let mut state = Start;
88
89    /// "Labels must be 63 characters or less."
90    const MAX_LABEL_LENGTH: usize = 63;
91
92    /// https://devblogs.microsoft.com/oldnewthing/20120412-00/?p=7873
93    const MAX_NAME_LENGTH: usize = 253;
94
95    if input.len() > MAX_NAME_LENGTH {
96        return Err(InvalidDnsNameError);
97    }
98
99    for ch in input {
100        state = match (state, ch) {
101            (Start | Next | NextAfterNumericOnly | Hyphen { .. }, b'.') => {
102                return Err(InvalidDnsNameError)
103            }
104            (Subsequent { .. }, b'.') => Next,
105            (NumericOnly { .. }, b'.') => NextAfterNumericOnly,
106            (Subsequent { len } | NumericOnly { len } | Hyphen { len }, _)
107                if len >= MAX_LABEL_LENGTH =>
108            {
109                return Err(InvalidDnsNameError)
110            }
111            (Start | Next | NextAfterNumericOnly, b'0'..=b'9') => NumericOnly { len: 1 },
112            (NumericOnly { len }, b'0'..=b'9') => NumericOnly { len: len + 1 },
113            (Start | Next | NextAfterNumericOnly, b'a'..=b'z' | b'A'..=b'Z' | b'_') => {
114                Subsequent { len: 1 }
115            }
116            (Subsequent { len } | NumericOnly { len } | Hyphen { len }, b'-') => {
117                Hyphen { len: len + 1 }
118            }
119            (
120                Subsequent { len } | NumericOnly { len } | Hyphen { len },
121                b'a'..=b'z' | b'A'..=b'Z' | b'_' | b'0'..=b'9',
122            ) => Subsequent { len: len + 1 },
123            _ => return Err(InvalidDnsNameError),
124        };
125    }
126
127    if matches!(
128        state,
129        Start | Hyphen { .. } | NumericOnly { .. } | NextAfterNumericOnly
130    ) {
131        return Err(InvalidDnsNameError);
132    }
133
134    Ok(())
135}
136
137enum State {
138    Start,
139    Next,
140    NumericOnly { len: usize },
141    NextAfterNumericOnly,
142    Subsequent { len: usize },
143    Hyphen { len: usize },
144}
145
146#[cfg(test)]
147mod test {
148    static TESTS: &[(&str, bool)] = &[
149        ("", false),
150        ("localhost", true),
151        ("LOCALHOST", true),
152        (".localhost", false),
153        ("..localhost", false),
154        ("1.2.3.4", false),
155        ("127.0.0.1", false),
156        ("absolute.", true),
157        ("absolute..", false),
158        ("multiple.labels.absolute.", true),
159        ("foo.bar.com", true),
160        ("infix-hyphen-allowed.com", true),
161        ("-prefixhypheninvalid.com", false),
162        ("suffixhypheninvalid--", false),
163        ("suffixhypheninvalid-.com", false),
164        ("foo.lastlabelendswithhyphen-", false),
165        ("infix_underscore_allowed.com", true),
166        ("_prefixunderscorevalid.com", true),
167        ("labelendswithnumber1.bar.com", true),
168        ("xn--bcher-kva.example", true),
169        (
170            "sixtythreesixtythreesixtythreesixtythreesixtythreesixtythreesix.com",
171            true,
172        ),
173        (
174            "sixtyfoursixtyfoursixtyfoursixtyfoursixtyfoursixtyfoursixtyfours.com",
175            false,
176        ),
177        (
178            "012345678901234567890123456789012345678901234567890123456789012.com",
179            true,
180        ),
181        (
182            "0123456789012345678901234567890123456789012345678901234567890123.com",
183            false,
184        ),
185        (
186            "01234567890123456789012345678901234567890123456789012345678901-.com",
187            false,
188        ),
189        (
190            "012345678901234567890123456789012345678901234567890123456789012-.com",
191            false,
192        ),
193        ("numeric-only-final-label.1", false),
194        ("numeric-only-final-label.absolute.1.", false),
195        ("1starts-with-number.com", true),
196        ("1Starts-with-number.com", true),
197        ("1.2.3.4.com", true),
198        ("123.numeric-only-first-label", true),
199        ("a123b.com", true),
200        ("numeric-only-middle-label.4.com", true),
201        ("1000-sans.badssl.com", true),
202        ("twohundredandfiftythreecharacters.twohundredandfiftythreecharacters.twohundredandfiftythreecharacters.twohundredandfiftythreecharacters.twohundredandfiftythreecharacters.twohundredandfiftythreecharacters.twohundredandfiftythreecharacters.twohundredandfi", true),
203        ("twohundredandfiftyfourcharacters.twohundredandfiftyfourcharacters.twohundredandfiftyfourcharacters.twohundredandfiftyfourcharacters.twohundredandfiftyfourcharacters.twohundredandfiftyfourcharacters.twohundredandfiftyfourcharacters.twohundredandfiftyfourc", false),
204    ];
205
206    #[test]
207    fn test_validation() {
208        for (input, expected) in TESTS {
209            println!("test: {:?} expected valid? {:?}", input, expected);
210            let name_ref = super::DnsNameRef::try_from(*input);
211            assert_eq!(*expected, name_ref.is_ok());
212            let name = super::DnsName::try_from(input.to_string());
213            assert_eq!(*expected, name.is_ok());
214        }
215    }
216
217    #[test]
218    fn error_is_debug() {
219        assert_eq!(
220            format!("{:?}", super::InvalidDnsNameError),
221            "InvalidDnsNameError"
222        );
223    }
224
225    #[test]
226    fn error_is_display() {
227        assert_eq!(
228            format!("{}", super::InvalidDnsNameError),
229            "invalid dns name"
230        );
231    }
232
233    #[test]
234    fn dns_name_is_debug() {
235        let example = super::DnsName::try_from("example.com".to_string()).unwrap();
236        assert_eq!(format!("{:?}", example), "DnsName(\"example.com\")");
237    }
238
239    #[test]
240    fn dns_name_traits() {
241        let example = super::DnsName::try_from("example.com".to_string()).unwrap();
242        assert_eq!(example, example); // PartialEq
243
244        use std::collections::HashSet;
245        let mut h = HashSet::<super::DnsName>::new();
246        h.insert(example);
247    }
248
249    #[test]
250    fn try_from_ascii_rejects_bad_utf8() {
251        assert_eq!(
252            format!("{:?}", super::DnsName::try_from_ascii(b"\x80")),
253            "Err(InvalidDnsNameError)"
254        );
255    }
256
257    #[test]
258    fn dns_name_ref_is_debug() {
259        let example = super::DnsNameRef::try_from("example.com").unwrap();
260        assert_eq!(format!("{:?}", example), "DnsNameRef(\"example.com\")");
261    }
262
263    #[test]
264    fn dns_name_ref_traits() {
265        let example = super::DnsNameRef::try_from("example.com").unwrap();
266        assert_eq!(example, example); // PartialEq
267
268        use std::collections::HashSet;
269        let mut h = HashSet::<super::DnsNameRef>::new();
270        h.insert(example);
271    }
272}