Our Personal Data Server from scratch!
0

Configure Feed

Select the types of activity you want to include in your feed.

handle: reject handles whose tld can never resolve

Lewis: May this revision serve well! <lu5a@proton.me>

author
Lewis
date (Jul 24, 2026, 8:27 PM +0300) commit 4da53ead parent a08e4756 change-id wotyotys
+93 -17
+2
crates/tranquil-pds/src/api/error.rs
··· 763 763 HandleValidationError::BannedWord => { 764 764 Self::InvalidHandle(Some("Inappropriate language in handle".to_string())) 765 765 } 766 + HandleValidationError::UnusableHandleDomain 767 + | HandleValidationError::NoHandleDomains => Self::InternalError(Some(e.to_string())), 766 768 _ => Self::InvalidHandle(Some(e.to_string())), 767 769 } 768 770 }
+76 -16
crates/tranquil-pds/src/api/validation.rs
··· 101 101 pub enum HandleValidationError { 102 102 Empty, 103 103 TooShort, 104 - TooLong, 104 + TooLong { max: usize }, 105 105 InvalidCharacters, 106 106 StartsWithInvalidChar, 107 107 EndsWithInvalidChar, 108 108 ContainsSpaces, 109 109 BannedWord, 110 110 Reserved, 111 + InvalidSyntax, 112 + DisallowedTld, 113 + UnusableHandleDomain, 114 + NoHandleDomains, 111 115 } 112 116 113 117 impl std::fmt::Display for HandleValidationError { ··· 119 123 "Handle must be at least {} characters", 120 124 MIN_HANDLE_LENGTH 121 125 ), 122 - Self::TooLong => write!( 123 - f, 124 - "Handle exceeds maximum length of {} characters", 125 - MAX_SERVICE_HANDLE_LOCAL_PART 126 - ), 126 + Self::TooLong { max } => { 127 + write!(f, "Handle exceeds maximum length of {} characters", max) 128 + } 127 129 Self::InvalidCharacters => write!( 128 130 f, 129 131 "Handle contains invalid characters. Only alphanumeric characters and hyphens are allowed" ··· 135 137 Self::ContainsSpaces => write!(f, "Handle cannot contain spaces"), 136 138 Self::BannedWord => write!(f, "Inappropriate language in handle"), 137 139 Self::Reserved => write!(f, "Reserved handle"), 140 + Self::InvalidSyntax => write!(f, "Handle does not match atproto handle syntax"), 141 + Self::DisallowedTld => write!(f, "Handle uses a reserved TLD and cannot resolve"), 142 + Self::UnusableHandleDomain => write!( 143 + f, 144 + "This server's handle domain has a reserved TLD, so no handle under it is a valid atproto handle" 145 + ), 146 + Self::NoHandleDomains => { 147 + write!(f, "No handle domains are configured on this server") 148 + } 138 149 } 139 150 } 140 151 } ··· 159 170 } 160 171 161 172 if handle.len() > MAX_HANDLE_LENGTH { 162 - return Err(HandleValidationError::TooLong); 173 + return Err(HandleValidationError::TooLong { 174 + max: MAX_HANDLE_LENGTH, 175 + }); 163 176 } 164 177 165 178 if handle ··· 190 203 return Err(HandleValidationError::BannedWord); 191 204 } 192 205 193 - Ok(Handle::from(handle_lower)) 206 + let handle = Handle::new(handle_lower).map_err(|_| HandleValidationError::InvalidSyntax)?; 207 + match handle.has_disallowed_tld() { 208 + true => Err(HandleValidationError::DisallowedTld), 209 + false => Ok(handle), 210 + } 194 211 } 195 212 196 213 pub fn validate_short_handle(handle: &str) -> Result<String, HandleValidationError> { ··· 210 227 None => input, 211 228 }; 212 229 let validated = validate_short_handle(handle_to_validate)?; 213 - Ok(Handle::from(format!( 214 - "{}.{}", 215 - validated, 216 - matched_domain.unwrap_or(&available_domains[0]) 217 - ))) 230 + let domain = matched_domain 231 + .or_else(|| available_domains.first()) 232 + .ok_or(HandleValidationError::NoHandleDomains)?; 233 + let handle = Handle::new(format!("{}.{}", validated, domain)) 234 + .map_err(|_| HandleValidationError::InvalidSyntax)?; 235 + match handle.has_disallowed_tld() { 236 + true => Err(HandleValidationError::UnusableHandleDomain), 237 + false => Ok(handle), 238 + } 218 239 } else { 219 240 validate_full_domain_handle(input) 220 241 } 221 242 } 222 243 244 + pub fn domain_forms_valid_handles(domain: &str) -> bool { 245 + Handle::new(format!("whelk.{domain}")).is_ok_and(|h| !h.has_disallowed_tld()) 246 + } 247 + 248 + pub fn warn_unusable_handle_domains() { 249 + tranquil_config::get() 250 + .server 251 + .user_handle_domain_list() 252 + .iter() 253 + .filter(|domain| !domain_forms_valid_handles(domain)) 254 + .for_each(|domain| { 255 + tracing::error!( 256 + domain = %domain, 257 + "configured handle domain can't form a valid atproto handle, so every account \ 258 + creation under it will be rejected. Set server.user_handle_domains to a domain \ 259 + whose TLD isn't reserved." 260 + ); 261 + }); 262 + } 263 + 223 264 pub fn validate_service_handle( 224 265 handle: &str, 225 266 reserved_policy: ReservedHandlePolicy, ··· 239 280 } 240 281 241 282 if handle.len() > MAX_SERVICE_HANDLE_LOCAL_PART { 242 - return Err(HandleValidationError::TooLong); 283 + return Err(HandleValidationError::TooLong { 284 + max: MAX_SERVICE_HANDLE_LOCAL_PART, 285 + }); 243 286 } 244 287 245 288 if let Some(first_char) = handle.chars().next() ··· 315 358 } 316 359 317 360 #[test] 361 + fn full_domain_handles_with_reserved_tlds_are_rejected() { 362 + assert!(validate_full_domain_handle("whelk.oyster.cafe").is_ok()); 363 + assert_eq!( 364 + validate_full_domain_handle("whelk.pds.internal"), 365 + Err(HandleValidationError::DisallowedTld) 366 + ); 367 + assert_eq!( 368 + validate_full_domain_handle("handle.invalid"), 369 + Err(HandleValidationError::DisallowedTld) 370 + ); 371 + } 372 + 373 + #[test] 318 374 fn test_invalid_handles() { 319 375 assert_eq!(validate_short_handle(""), Err(HandleValidationError::Empty)); 320 376 assert_eq!( ··· 388 444 ); 389 445 assert_eq!( 390 446 validate_short_handle("exactly19characters"), 391 - Err(HandleValidationError::TooLong) 447 + Err(HandleValidationError::TooLong { 448 + max: MAX_SERVICE_HANDLE_LOCAL_PART 449 + }) 392 450 ); 393 451 assert_eq!( 394 452 validate_short_handle("waytoolongusername123456789"), 395 - Err(HandleValidationError::TooLong) 453 + Err(HandleValidationError::TooLong { 454 + max: MAX_SERVICE_HANDLE_LOCAL_PART 455 + }) 396 456 ); 397 457 } 398 458
+3 -1
crates/tranquil-pds/tests/validation_edge_cases.rs
··· 217 217 let over_max = "a".repeat(MAX_SERVICE_HANDLE_LOCAL_PART + 1); 218 218 assert!(matches!( 219 219 validate_short_handle(&over_max), 220 - Err(HandleValidationError::TooLong) 220 + Err(HandleValidationError::TooLong { 221 + max: MAX_SERVICE_HANDLE_LOCAL_PART 222 + }) 221 223 )); 222 224 } 223 225
+12
crates/tranquil-server/src/main.rs
··· 75 75 eprintln!("TLS material invalid: {e}"); 76 76 return ExitCode::FAILURE; 77 77 } 78 + config 79 + .server 80 + .user_handle_domain_list() 81 + .iter() 82 + .filter(|d| !tranquil_pds::api::validation::domain_forms_valid_handles(d)) 83 + .for_each(|d| { 84 + eprintln!( 85 + "account creation under handle domain {d} will be rejected because its TLD is reserved" 86 + ); 87 + }); 78 88 println!("Configuration is valid."); 79 89 ExitCode::SUCCESS 80 90 } ··· 104 114 } 105 115 106 116 tranquil_config::init(config); 117 + 118 + tranquil_pds::api::validation::warn_unusable_handle_domains(); 107 119 108 120 tranquil_pds::metrics::init_metrics(); 109 121