#[cfg(feature = "alloc")]
use alloc::string::String;
use core::fmt::Write;
use crate::Error;
#[cfg(feature = "alloc")]
#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub struct DnsName(String);
#[cfg(feature = "alloc")]
#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
impl DnsName {
pub fn as_ref(&self) -> DnsNameRef {
DnsNameRef(self.0.as_bytes())
}
}
#[cfg(feature = "alloc")]
#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
impl AsRef<str> for DnsName {
fn as_ref(&self) -> &str {
self.0.as_ref()
}
}
#[derive(Clone, Copy, Eq, PartialEq, Hash)]
pub struct DnsNameRef<'a>(pub(crate) &'a [u8]);
impl AsRef<str> for DnsNameRef<'_> {
#[inline]
fn as_ref(&self) -> &str {
core::str::from_utf8(self.0).unwrap()
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct InvalidDnsNameError;
impl core::fmt::Display for InvalidDnsNameError {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
write!(f, "{:?}", self)
}
}
#[cfg(feature = "std")]
#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
impl ::std::error::Error for InvalidDnsNameError {}
impl<'a> DnsNameRef<'a> {
pub fn try_from_ascii(dns_name: &'a [u8]) -> Result<Self, InvalidDnsNameError> {
if !is_valid_dns_id(
untrusted::Input::from(dns_name),
IdRole::Reference,
AllowWildcards::No,
) {
return Err(InvalidDnsNameError);
}
Ok(Self(dns_name))
}
pub fn try_from_ascii_str(dns_name: &'a str) -> Result<Self, InvalidDnsNameError> {
Self::try_from_ascii(dns_name.as_bytes())
}
#[cfg(feature = "alloc")]
#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
pub fn to_owned(&self) -> DnsName {
let s: &str = (*self).into();
DnsName(s.to_ascii_lowercase())
}
}
impl core::fmt::Debug for DnsNameRef<'_> {
fn fmt(&self, f: &mut core::fmt::Formatter) -> Result<(), core::fmt::Error> {
f.write_str("DnsNameRef(\"")?;
for &ch in self.0 {
f.write_char(char::from(ch).to_ascii_lowercase())?;
}
f.write_str("\")")
}
}
impl<'a> From<DnsNameRef<'a>> for &'a str {
fn from(DnsNameRef(d): DnsNameRef<'a>) -> Self {
core::str::from_utf8(d).unwrap()
}
}
pub enum GeneralDnsNameRef<'name> {
DnsName(DnsNameRef<'name>),
Wildcard(WildcardDnsNameRef<'name>),
}
impl<'a> From<GeneralDnsNameRef<'a>> for &'a str {
fn from(d: GeneralDnsNameRef<'a>) -> Self {
match d {
GeneralDnsNameRef::DnsName(name) => name.into(),
GeneralDnsNameRef::Wildcard(name) => name.into(),
}
}
}
#[derive(Clone, Copy, Eq, PartialEq, Hash)]
pub struct WildcardDnsNameRef<'a>(&'a [u8]);
impl<'a> WildcardDnsNameRef<'a> {
pub fn try_from_ascii(dns_name: &'a [u8]) -> Result<Self, InvalidDnsNameError> {
if !is_valid_dns_id(
untrusted::Input::from(dns_name),
IdRole::Reference,
AllowWildcards::Yes,
) {
return Err(InvalidDnsNameError);
}
Ok(Self(dns_name))
}
pub fn try_from_ascii_str(dns_name: &'a str) -> Result<Self, InvalidDnsNameError> {
Self::try_from_ascii(dns_name.as_bytes())
}
}
impl core::fmt::Debug for WildcardDnsNameRef<'_> {
fn fmt(&self, f: &mut core::fmt::Formatter) -> Result<(), core::fmt::Error> {
f.write_str("WildcardDnsNameRef(\"")?;
for &ch in self.0 {
f.write_char(char::from(ch).to_ascii_lowercase())?;
}
f.write_str("\")")
}
}
impl<'a> From<WildcardDnsNameRef<'a>> for &'a str {
fn from(WildcardDnsNameRef(d): WildcardDnsNameRef<'a>) -> Self {
core::str::from_utf8(d).unwrap()
}
}
impl AsRef<str> for WildcardDnsNameRef<'_> {
#[inline]
fn as_ref(&self) -> &str {
core::str::from_utf8(self.0).unwrap()
}
}
pub(super) fn presented_id_matches_reference_id(
presented_dns_id: untrusted::Input,
reference_dns_id: untrusted::Input,
) -> Result<bool, Error> {
presented_id_matches_reference_id_internal(
presented_dns_id,
IdRole::Reference,
reference_dns_id,
)
}
pub(super) fn presented_id_matches_constraint(
presented_dns_id: untrusted::Input,
reference_dns_id: untrusted::Input,
) -> Result<bool, Error> {
presented_id_matches_reference_id_internal(
presented_dns_id,
IdRole::NameConstraint,
reference_dns_id,
)
}
fn presented_id_matches_reference_id_internal(
presented_dns_id: untrusted::Input,
reference_dns_id_role: IdRole,
reference_dns_id: untrusted::Input,
) -> Result<bool, Error> {
if !is_valid_dns_id(presented_dns_id, IdRole::Presented, AllowWildcards::Yes) {
return Err(Error::MalformedDnsIdentifier);
}
if !is_valid_dns_id(reference_dns_id, reference_dns_id_role, AllowWildcards::No) {
return Err(match reference_dns_id_role {
IdRole::NameConstraint => Error::MalformedNameConstraint,
_ => Error::MalformedDnsIdentifier,
});
}
let mut presented = untrusted::Reader::new(presented_dns_id);
let mut reference = untrusted::Reader::new(reference_dns_id);
match reference_dns_id_role {
IdRole::Reference => (),
IdRole::NameConstraint if presented_dns_id.len() > reference_dns_id.len() => {
if reference_dns_id.is_empty() {
return Ok(true);
}
if reference.peek(b'.') {
if presented
.skip(presented_dns_id.len() - reference_dns_id.len())
.is_err()
{
unreachable!();
}
} else {
if presented
.skip(presented_dns_id.len() - reference_dns_id.len() - 1)
.is_err()
{
unreachable!();
}
if presented.read_byte() != Ok(b'.') {
return Ok(false);
}
}
}
IdRole::NameConstraint => (),
IdRole::Presented => unreachable!(),
}
if presented.peek(b'*') {
if presented.skip(1).is_err() {
unreachable!();
}
loop {
if reference.read_byte().is_err() {
return Ok(false);
}
if reference.peek(b'.') {
break;
}
}
}
loop {
let presented_byte = match (presented.read_byte(), reference.read_byte()) {
(Ok(p), Ok(r)) if ascii_lower(p) == ascii_lower(r) => p,
_ => {
return Ok(false);
}
};
if presented.at_end() {
if presented_byte == b'.' {
return Err(Error::MalformedDnsIdentifier);
}
break;
}
}
if !reference.at_end() {
if reference_dns_id_role != IdRole::NameConstraint {
match reference.read_byte() {
Ok(b'.') => (),
_ => {
return Ok(false);
}
};
}
if !reference.at_end() {
return Ok(false);
}
}
assert!(presented.at_end());
assert!(reference.at_end());
Ok(true)
}
#[inline]
fn ascii_lower(b: u8) -> u8 {
match b {
b'A'..=b'Z' => b + b'a' - b'A',
_ => b,
}
}
#[derive(Clone, Copy, PartialEq)]
enum AllowWildcards {
No,
Yes,
}
#[derive(Clone, Copy, PartialEq)]
enum IdRole {
Reference,
Presented,
NameConstraint,
}
fn is_valid_dns_id(
hostname: untrusted::Input,
id_role: IdRole,
allow_wildcards: AllowWildcards,
) -> bool {
if hostname.len() > 253 {
return false;
}
let mut input = untrusted::Reader::new(hostname);
if id_role == IdRole::NameConstraint && input.at_end() {
return true;
}
let mut dot_count = 0;
let mut label_length = 0;
let mut label_is_all_numeric = false;
let mut label_ends_with_hyphen = false;
let is_wildcard = allow_wildcards == AllowWildcards::Yes && input.peek(b'*');
let mut is_first_byte = !is_wildcard;
if is_wildcard {
if input.read_byte() != Ok(b'*') || input.read_byte() != Ok(b'.') {
return false;
}
dot_count += 1;
}
loop {
const MAX_LABEL_LENGTH: usize = 63;
match input.read_byte() {
Ok(b'-') => {
if label_length == 0 {
return false; }
label_is_all_numeric = false;
label_ends_with_hyphen = true;
label_length += 1;
if label_length > MAX_LABEL_LENGTH {
return false;
}
}
Ok(b'0'..=b'9') => {
if label_length == 0 {
label_is_all_numeric = true;
}
label_ends_with_hyphen = false;
label_length += 1;
if label_length > MAX_LABEL_LENGTH {
return false;
}
}
Ok(b'a'..=b'z') | Ok(b'A'..=b'Z') | Ok(b'_') => {
label_is_all_numeric = false;
label_ends_with_hyphen = false;
label_length += 1;
if label_length > MAX_LABEL_LENGTH {
return false;
}
}
Ok(b'.') => {
dot_count += 1;
if label_length == 0 && (id_role != IdRole::NameConstraint || !is_first_byte) {
return false;
}
if label_ends_with_hyphen {
return false; }
label_length = 0;
}
_ => {
return false;
}
}
is_first_byte = false;
if input.at_end() {
break;
}
}
if label_length == 0 && id_role != IdRole::Reference {
return false;
}
if label_ends_with_hyphen {
return false; }
if label_is_all_numeric {
return false; }
if is_wildcard {
let label_count = if label_length == 0 {
dot_count
} else {
dot_count + 1
};
if label_count < 3 {
return false;
}
}
true
}
#[cfg(test)]
mod tests {
use super::*;
#[allow(clippy::type_complexity)]
const PRESENTED_MATCHES_REFERENCE: &[(&[u8], &[u8], Result<bool, Error>)] = &[
(b"", b"a", Err(Error::MalformedDnsIdentifier)),
(b"a", b"a", Ok(true)),
(b"b", b"a", Ok(false)),
(b"*.b.a", b"c.b.a", Ok(true)),
(b"*.b.a", b"b.a", Ok(false)),
(b"*.b.a", b"b.a.", Ok(false)),
(b"d.c.b.a", b"d.c.b.a", Ok(true)),
(b"d.*.b.a", b"d.c.b.a", Err(Error::MalformedDnsIdentifier)),
(b"d.c*.b.a", b"d.c.b.a", Err(Error::MalformedDnsIdentifier)),
(b"d.c*.b.a", b"d.cc.b.a", Err(Error::MalformedDnsIdentifier)),
(
b"abcdefghijklmnopqrstuvwxyz",
b"ABCDEFGHIJKLMNOPQRSTUVWXYZ",
Ok(true),
),
(
b"ABCDEFGHIJKLMNOPQRSTUVWXYZ",
b"abcdefghijklmnopqrstuvwxyz",
Ok(true),
),
(b"aBc", b"Abc", Ok(true)),
(b"a1", b"a1", Ok(true)),
(b"example", b"example", Ok(true)),
(b"example.", b"example.", Err(Error::MalformedDnsIdentifier)),
(b"example", b"example.", Ok(true)),
(b"example.", b"example", Err(Error::MalformedDnsIdentifier)),
(b"example.com", b"example.com", Ok(true)),
(
b"example.com.",
b"example.com.",
Err(Error::MalformedDnsIdentifier),
),
(b"example.com", b"example.com.", Ok(true)),
(
b"example.com.",
b"example.com",
Err(Error::MalformedDnsIdentifier),
),
(
b"example.com..",
b"example.com.",
Err(Error::MalformedDnsIdentifier),
),
(
b"example.com..",
b"example.com",
Err(Error::MalformedDnsIdentifier),
),
(
b"example.com...",
b"example.com.",
Err(Error::MalformedDnsIdentifier),
),
(b"x*.b.a", b"xa.b.a", Err(Error::MalformedDnsIdentifier)),
(b"x*.b.a", b"xna.b.a", Err(Error::MalformedDnsIdentifier)),
(b"x*.b.a", b"xn-a.b.a", Err(Error::MalformedDnsIdentifier)),
(b"x*.b.a", b"xn--a.b.a", Err(Error::MalformedDnsIdentifier)),
(b"xn*.b.a", b"xn--a.b.a", Err(Error::MalformedDnsIdentifier)),
(
b"xn-*.b.a",
b"xn--a.b.a",
Err(Error::MalformedDnsIdentifier),
),
(
b"xn--*.b.a",
b"xn--a.b.a",
Err(Error::MalformedDnsIdentifier),
),
(b"xn*.b.a", b"xn--a.b.a", Err(Error::MalformedDnsIdentifier)),
(
b"xn-*.b.a",
b"xn--a.b.a",
Err(Error::MalformedDnsIdentifier),
),
(
b"xn--*.b.a",
b"xn--a.b.a",
Err(Error::MalformedDnsIdentifier),
),
(
b"xn---*.b.a",
b"xn--a.b.a",
Err(Error::MalformedDnsIdentifier),
),
(b"c*.b.a", b"c.b.a", Err(Error::MalformedDnsIdentifier)),
(b"foo.com", b"foo.com", Ok(true)),
(b"f", b"f", Ok(true)),
(b"i", b"h", Ok(false)),
(b"*.foo.com", b"bar.foo.com", Ok(true)),
(b"*.test.fr", b"www.test.fr", Ok(true)),
(b"*.test.FR", b"wwW.tESt.fr", Ok(true)),
(b".uk", b"f.uk", Err(Error::MalformedDnsIdentifier)),
(
b"?.bar.foo.com",
b"w.bar.foo.com",
Err(Error::MalformedDnsIdentifier),
),
(
b"(www|ftp).foo.com",
b"www.foo.com",
Err(Error::MalformedDnsIdentifier),
), (
b"www.foo.com\0",
b"www.foo.com",
Err(Error::MalformedDnsIdentifier),
),
(
b"www.foo.com\0*.foo.com",
b"www.foo.com",
Err(Error::MalformedDnsIdentifier),
),
(b"ww.house.example", b"www.house.example", Ok(false)),
(b"www.test.org", b"test.org", Ok(false)),
(b"*.test.org", b"test.org", Ok(false)),
(b"*.org", b"test.org", Err(Error::MalformedDnsIdentifier)),
(
b"w*.bar.foo.com",
b"w.bar.foo.com",
Err(Error::MalformedDnsIdentifier),
),
(
b"ww*ww.bar.foo.com",
b"www.bar.foo.com",
Err(Error::MalformedDnsIdentifier),
),
(
b"ww*ww.bar.foo.com",
b"wwww.bar.foo.com",
Err(Error::MalformedDnsIdentifier),
),
(
b"w*w.bar.foo.com",
b"wwww.bar.foo.com",
Err(Error::MalformedDnsIdentifier),
),
(
b"w*w.bar.foo.c0m",
b"wwww.bar.foo.com",
Err(Error::MalformedDnsIdentifier),
),
(
b"wa*.bar.foo.com",
b"WALLY.bar.foo.com",
Err(Error::MalformedDnsIdentifier),
),
(
b"*Ly.bar.foo.com",
b"wally.bar.foo.com",
Err(Error::MalformedDnsIdentifier),
),
(b"*.test.de", b"www.test.co.jp", Ok(false)),
(
b"*.jp",
b"www.test.co.jp",
Err(Error::MalformedDnsIdentifier),
),
(b"www.test.co.uk", b"www.test.co.jp", Ok(false)),
(
b"www.*.co.jp",
b"www.test.co.jp",
Err(Error::MalformedDnsIdentifier),
),
(b"www.bar.foo.com", b"www.bar.foo.com", Ok(true)),
(b"*.foo.com", b"www.bar.foo.com", Ok(false)),
(
b"*.*.foo.com",
b"www.bar.foo.com",
Err(Error::MalformedDnsIdentifier),
),
(b"www.bath.org", b"www.bath.org", Ok(true)),
(
b"xn--poema-9qae5a.com.br",
b"xn--poema-9qae5a.com.br",
Ok(true),
),
(
b"*.xn--poema-9qae5a.com.br",
b"www.xn--poema-9qae5a.com.br",
Ok(true),
),
(
b"*.xn--poema-9qae5a.com.br",
b"xn--poema-9qae5a.com.br",
Ok(false),
),
(
b"xn--poema-*.com.br",
b"xn--poema-9qae5a.com.br",
Err(Error::MalformedDnsIdentifier),
),
(
b"xn--*-9qae5a.com.br",
b"xn--poema-9qae5a.com.br",
Err(Error::MalformedDnsIdentifier),
),
(
b"*--poema-9qae5a.com.br",
b"xn--poema-9qae5a.com.br",
Err(Error::MalformedDnsIdentifier),
),
(b"*.example.com", b"foo.example.com", Ok(true)),
(b"*.example.com", b"bar.foo.example.com", Ok(false)),
(b"*.example.com", b"example.com", Ok(false)),
(
b"baz*.example.net",
b"baz1.example.net",
Err(Error::MalformedDnsIdentifier),
),
(
b"*baz.example.net",
b"foobaz.example.net",
Err(Error::MalformedDnsIdentifier),
),
(
b"b*z.example.net",
b"buzz.example.net",
Err(Error::MalformedDnsIdentifier),
),
(b"*.test.example", b"www.test.example", Ok(true)),
(b"*.example.co.uk", b"test.example.co.uk", Ok(true)),
(
b"*.example",
b"test.example",
Err(Error::MalformedDnsIdentifier),
),
(b"*.co.uk", b"example.co.uk", Ok(true)),
(b"*.com", b"foo.com", Err(Error::MalformedDnsIdentifier)),
(b"*.us", b"foo.us", Err(Error::MalformedDnsIdentifier)),
(b"*", b"foo", Err(Error::MalformedDnsIdentifier)),
(
b"*.xn--poema-9qae5a.com.br",
b"www.xn--poema-9qae5a.com.br",
Ok(true),
),
(
b"*.example.xn--mgbaam7a8h",
b"test.example.xn--mgbaam7a8h",
Ok(true),
),
(b"*.com.br", b"xn--poema-9qae5a.com.br", Ok(true)),
(
b"*.xn--mgbaam7a8h",
b"example.xn--mgbaam7a8h",
Err(Error::MalformedDnsIdentifier),
),
(b"*.appspot.com", b"www.appspot.com", Ok(true)),
(b"*.s3.amazonaws.com", b"foo.s3.amazonaws.com", Ok(true)),
(
b"*.*.com",
b"foo.example.com",
Err(Error::MalformedDnsIdentifier),
),
(
b"*.bar.*.com",
b"foo.bar.example.com",
Err(Error::MalformedDnsIdentifier),
),
(b"foo.com.", b"foo.com", Err(Error::MalformedDnsIdentifier)),
(b"foo.com", b"foo.com.", Ok(true)),
(b"foo.com.", b"foo.com.", Err(Error::MalformedDnsIdentifier)),
(b"f.", b"f", Err(Error::MalformedDnsIdentifier)),
(b"f", b"f.", Ok(true)),
(b"f.", b"f.", Err(Error::MalformedDnsIdentifier)),
(
b"*.bar.foo.com.",
b"www-3.bar.foo.com",
Err(Error::MalformedDnsIdentifier),
),
(b"*.bar.foo.com", b"www-3.bar.foo.com.", Ok(true)),
(
b"*.bar.foo.com.",
b"www-3.bar.foo.com.",
Err(Error::MalformedDnsIdentifier),
),
(
b"*.com.",
b"example.com",
Err(Error::MalformedDnsIdentifier),
),
(
b"*.com",
b"example.com.",
Err(Error::MalformedDnsIdentifier),
),
(
b"*.com.",
b"example.com.",
Err(Error::MalformedDnsIdentifier),
),
(b"*.", b"foo.", Err(Error::MalformedDnsIdentifier)),
(b"*.", b"foo", Err(Error::MalformedDnsIdentifier)),
(
b"*.co.uk.",
b"foo.co.uk",
Err(Error::MalformedDnsIdentifier),
),
(
b"*.co.uk.",
b"foo.co.uk.",
Err(Error::MalformedDnsIdentifier),
),
];
#[test]
fn presented_matches_reference_test() {
for &(presented, reference, expected_result) in PRESENTED_MATCHES_REFERENCE {
let actual_result = presented_id_matches_reference_id(
untrusted::Input::from(presented),
untrusted::Input::from(reference),
);
assert_eq!(
actual_result, expected_result,
"presented_id_matches_reference_id(\"{:?}\", \"{:?}\")",
presented, reference
);
}
}
#[allow(clippy::type_complexity)]
const PRESENTED_MATCHES_CONSTRAINT: &[(&[u8], &[u8], Result<bool, Error>)] = &[
(b".", b"", Err(Error::MalformedDnsIdentifier)),
(b"www.example.com.", b"", Err(Error::MalformedDnsIdentifier)),
(
b"www.example.com.",
b"www.example.com.",
Err(Error::MalformedDnsIdentifier),
),
(
b"www.example.com",
b".",
Err(Error::MalformedNameConstraint),
),
(
b"www.example.com",
b"www.example.com.",
Err(Error::MalformedNameConstraint),
),
(
b"www.example.com",
b"*.example.com",
Err(Error::MalformedNameConstraint),
),
(b"", b"", Err(Error::MalformedDnsIdentifier)),
(b"example.com", b"", Ok(true)),
(b"*.example.com", b"", Ok(true)),
(b"www.example.com", b".example.com", Ok(true)),
(b"www.example.com", b".EXAMPLE.COM", Ok(true)),
(b"www.example.com", b".axample.com", Ok(false)),
(b"www.example.com", b".xample.com", Ok(false)),
(b"www.example.com", b".exampl.com", Ok(false)),
(b"badexample.com", b".example.com", Ok(false)),
(b"www.example.com", b"example.com", Ok(true)),
(b"www.example.com", b"EXAMPLE.COM", Ok(true)),
(b"www.example.com", b"axample.com", Ok(false)),
(b"www.example.com", b"xample.com", Ok(false)),
(b"www.example.com", b"exampl.com", Ok(false)),
(b"badexample.com", b"example.com", Ok(false)),
(b"*.example.com", b".example.com", Ok(true)),
(b"*.example.com", b"example.com", Ok(true)),
(b"*.example.com", b"www.example.com", Ok(true)),
(b"*.example.com", b"www.EXAMPLE.COM", Ok(true)),
(b"*.example.com", b"www.axample.com", Ok(false)),
(b"*.example.com", b".xample.com", Ok(false)),
(b"*.example.com", b"xample.com", Ok(false)),
(b"*.example.com", b".exampl.com", Ok(false)),
(b"*.example.com", b"exampl.com", Ok(false)),
(b"www.example.com", b"www.example.com", Ok(true)),
];
#[test]
fn presented_matches_constraint_test() {
for &(presented, constraint, expected_result) in PRESENTED_MATCHES_CONSTRAINT {
let actual_result = presented_id_matches_constraint(
untrusted::Input::from(presented),
untrusted::Input::from(constraint),
);
assert_eq!(
actual_result, expected_result,
"presented_id_matches_constraint(\"{:?}\", \"{:?}\")",
presented, constraint,
);
}
}
}