#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct WapSNI(String);
impl WapSNI {
const MIN_LABELS: usize = 3;
pub fn new(mut sni: String) -> Result<Self, SniFormatError> {
sni.make_ascii_lowercase();
validate_hostname(&sni, Self::MIN_LABELS)?;
Ok(Self(sni))
}
pub fn wap_id(&self) -> &str {
self.0
.split('.')
.next()
.expect("SNI has at least 3 segments")
}
pub fn wap_namespace(&self) -> &str {
self.0
.split('.')
.nth(1)
.expect("SNI has at least 3 segments")
}
pub fn customer_domain(&self) -> CustomerDomainRef<'_> {
CustomerDomainRef(
self.0
.splitn(3, '.')
.last()
.expect("SNI has at least 3 segments"),
)
}
pub const GATEWAY_DOMAIN_SUFFIX: &str = "wg";
pub fn gateway_domain(&self) -> GatewayDomain {
GatewayDomain(format!(
"{}-{}.{}",
Self::GATEWAY_DOMAIN_SUFFIX,
self.wap_namespace(),
self.customer_domain()
))
}
pub fn full_domain(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for WapSNI {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct CustomerDomain(String);
impl CustomerDomain {
const MIN_LABELS: usize = 1;
pub fn new(mut domain: String) -> Result<Self, SniFormatError> {
domain.make_ascii_lowercase();
validate_hostname(&domain, Self::MIN_LABELS)?;
Ok(Self(domain))
}
pub fn as_domain(&self) -> CustomerDomainRef<'_> {
CustomerDomainRef(&self.0)
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::borrow::Borrow<str> for CustomerDomain {
fn borrow(&self) -> &str {
&self.0
}
}
impl From<CustomerDomainRef<'_>> for CustomerDomain {
fn from(domain: CustomerDomainRef<'_>) -> Self {
Self(domain.0.to_owned())
}
}
impl std::fmt::Display for CustomerDomain {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct CustomerDomainRef<'a>(&'a str);
impl<'a> CustomerDomainRef<'a> {
pub fn as_str(&self) -> &'a str {
self.0
}
}
impl std::fmt::Display for CustomerDomainRef<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct GatewayDomain(String);
impl GatewayDomain {
const MIN_LABELS: usize = 2;
pub fn new(mut domain: String) -> Result<Self, SniFormatError> {
domain.make_ascii_lowercase();
validate_hostname(&domain, Self::MIN_LABELS)?;
Ok(Self(domain))
}
pub fn as_domain(&self) -> GatewayDomainRef<'_> {
GatewayDomainRef(&self.0)
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for GatewayDomain {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct GatewayDomainRef<'a>(&'a str);
impl<'a> GatewayDomainRef<'a> {
pub fn as_str(&self) -> &'a str {
self.0
}
}
impl From<GatewayDomainRef<'_>> for GatewayDomain {
fn from(domain: GatewayDomainRef<'_>) -> Self {
Self(domain.0.to_owned())
}
}
impl std::fmt::Display for GatewayDomainRef<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
const MAX_NAME_LEN: usize = 253;
const MAX_LABEL_LEN: usize = 63;
fn validate_hostname(name: &str, min_labels: usize) -> Result<(), SniFormatError> {
if name.len() > MAX_NAME_LEN {
return Err(SniFormatError::NameTooLong);
}
if name.split('.').count() < min_labels {
return Err(SniFormatError::TooFewSegments);
}
for label in name.split('.') {
if label.is_empty() {
return Err(SniFormatError::EmptyLabel);
}
if label.len() > MAX_LABEL_LEN {
return Err(SniFormatError::LabelTooLong(label.to_owned()));
}
if !label
.bytes()
.all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-')
{
return Err(SniFormatError::InvalidCharacters(label.to_owned()));
}
}
Ok(())
}
#[derive(thiserror::Error, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum SniFormatError {
#[error("the SNI is not in the expected format: <wap-id>.<wap-namespace>.<customer-domain>")]
TooFewSegments,
#[error("the SNI is longer than {max} characters", max = MAX_NAME_LEN)]
NameTooLong,
#[error("the SNI contains an empty segment")]
EmptyLabel,
#[error("the segment {0:?} is longer than {max} characters", max = MAX_LABEL_LEN)]
LabelTooLong(String),
#[error("the segment {0:?} contains characters outside of [a-z0-9-]")]
InvalidCharacters(String),
#[error("the SNI is an address literal")]
AddressLiteral,
}
#[cfg(test)]
mod tests {
use super::{CustomerDomain, GatewayDomain, MAX_LABEL_LEN, SniFormatError, WapSNI};
fn sni(s: &str) -> Result<WapSNI, SniFormatError> {
WapSNI::new(s.to_owned())
}
#[test]
fn accepts_valid_names() {
for name in [
"id.wap.example.com",
"id.wap.example.co.uk",
"id-1.wap-2.example.com",
"0.0.example.com",
"id.wap.xn--bcher-kva.example",
&format!("{}.wap.example.com", "a".repeat(MAX_LABEL_LEN)),
] {
assert!(sni(name).is_ok(), "{name} should be accepted");
}
}
#[test]
fn splits_into_segments() {
let sni = sni("id.wap.example.com").expect("valid SNI");
assert_eq!(sni.wap_id(), "id");
assert_eq!(sni.wap_namespace(), "wap");
assert_eq!(sni.customer_domain().as_str(), "example.com");
assert_eq!(sni.gateway_domain().as_str(), "wg-wap.example.com");
assert_eq!(sni.full_domain(), "id.wap.example.com");
}
#[test]
fn the_customer_domain_of_an_sni_equals_the_owned_one() {
let sni = sni("id.wap.example.com").expect("valid SNI");
let owned = CustomerDomain::new("example.com".to_owned()).expect("valid domain");
assert_eq!(CustomerDomain::from(sni.customer_domain()), owned);
assert_eq!(owned.as_domain(), sni.customer_domain());
assert_eq!(owned.to_string(), "example.com");
}
#[test]
fn domains_are_validated_like_the_sni_but_may_be_shorter() {
assert!(CustomerDomain::new("backend".to_owned()).is_ok());
assert_eq!(
CustomerDomain::new("back end".to_owned()).unwrap_err(),
SniFormatError::InvalidCharacters("back end".to_owned()),
);
assert!(GatewayDomain::new("wg-wap.example.com".to_owned()).is_ok());
assert_eq!(
GatewayDomain::new("wg".to_owned()).unwrap_err(),
SniFormatError::TooFewSegments,
);
}
#[test]
fn lowercases_the_name() {
let sni = sni("ID.Wap.Example.COM").expect("valid SNI");
assert_eq!(sni.full_domain(), "id.wap.example.com");
assert_eq!(sni, WapSNI::new("id.wap.example.com".to_owned()).unwrap());
}
#[test]
fn rejects_invalid_names() {
for (name, want) in [
("id.wap", SniFormatError::TooFewSegments),
("example.com", SniFormatError::TooFewSegments),
("", SniFormatError::TooFewSegments),
("id.wap.example.com.", SniFormatError::EmptyLabel),
("id.wap..com", SniFormatError::EmptyLabel),
(".wap.example.com", SniFormatError::EmptyLabel),
(
"id.wap.example.com/evil",
SniFormatError::InvalidCharacters("com/evil".to_owned()),
),
(
"id.wap.example.com:443",
SniFormatError::InvalidCharacters("com:443".to_owned()),
),
(
"id.wap.example.com?a=b",
SniFormatError::InvalidCharacters("com?a=b".to_owned()),
),
(
"id.wap.example.com#f",
SniFormatError::InvalidCharacters("com#f".to_owned()),
),
(
"id.wap@evil.example.com",
SniFormatError::InvalidCharacters("wap@evil".to_owned()),
),
(
"id.wap.exämple.com",
SniFormatError::InvalidCharacters("exämple".to_owned()),
),
(
"id.wap.example .com",
SniFormatError::InvalidCharacters("example ".to_owned()),
),
] {
assert_eq!(sni(name).unwrap_err(), want, "for {name:?}");
}
}
#[test]
fn rejects_oversized_names() {
let long_label = "a".repeat(MAX_LABEL_LEN + 1);
assert_eq!(
sni(&format!("{long_label}.wap.example.com")).unwrap_err(),
SniFormatError::LabelTooLong(long_label),
);
let label = "a".repeat(MAX_LABEL_LEN);
let long_name = [label.as_str(); 4].join(".");
assert_eq!(long_name.len(), 255);
assert_eq!(sni(&long_name).unwrap_err(), SniFormatError::NameTooLong);
}
}