didbot_serve/tree/
names.rs1use axum::http::StatusCode;
5use didbot_pds::Registry;
6
7use crate::error::ApiError;
8
9pub const RESERVED_LABELS: &[&str] = &[
14 "www",
15 "xrpc",
16 "dashboard",
17 "api",
18 "admin",
19 "mail",
20 "mx",
21 "smtp",
22 "ns",
23 "ns1",
24 "ns2",
25 "localhost",
26 "wpad",
27 "isatap",
28 "autoconfig",
29 "autodiscover",
30];
31
32const MAX_LABEL_LEN: usize = 63;
34
35const MAX_NAME_LEN: usize = 253;
37
38pub(super) fn split_name(
47 registry: &dyn Registry,
48 reserved: &[String],
49 name: &str,
50) -> Result<(String, String), ApiError> {
51 let host = name.split(':').next().unwrap_or(name).to_ascii_lowercase();
52 let mut domains = registry.available_user_domains();
53 domains.sort_by_key(|domain| std::cmp::Reverse(domain.len()));
54 let invalid = |why: String| ApiError::new(StatusCode::BAD_REQUEST, "InvalidName", why);
55 if domains
56 .iter()
57 .any(|domain| domain.trim_start_matches('.') == host)
58 {
59 return Err(invalid(format!(
60 "{name:?} is one of this server's zones, not a name an account may take"
61 )));
62 }
63 let domain = domains
64 .iter()
65 .find(|domain| host.ends_with(domain.as_str()))
66 .ok_or_else(|| {
67 ApiError::new(
68 StatusCode::BAD_REQUEST,
69 "NameOutsideZone",
70 format!(
71 "{name:?} is not under a zone this server serves ({})",
72 domains.join(", ")
73 ),
74 )
75 })?;
76 let zone = domain.trim_start_matches('.');
77 let label = &host[..host.len() - domain.len()];
78 if let Some((_, parent)) = label.split_once('.') {
79 return Err(invalid(format!(
80 "{name:?} is more than one label under {zone}, and a zone's certificate covers \
81 one; name it one label under a zone, or add {parent}.{zone} as a zone"
82 )));
83 }
84 if label.is_empty() || didbot_identity::validate_label(label).is_err() {
85 return Err(invalid(format!("{name:?} must be one label under {zone}")));
86 }
87 if host.len() > MAX_NAME_LEN || label.len() > MAX_LABEL_LEN {
88 return Err(invalid(format!(
89 "{name:?} is longer than DNS carries: a label is at most {MAX_LABEL_LEN} bytes and \
90 a name {MAX_NAME_LEN}"
91 )));
92 }
93 if reserved.iter().any(|reserved| reserved == label) {
94 return Err(invalid(format!(
95 "{name:?} takes {label:?}, a label this deployment keeps for itself"
96 )));
97 }
98 Ok((label.to_owned(), zone.to_owned()))
99}