//! Domain Verification - Custom domain ownership proof //! //! Verifies that users own the domains they want to link to their names //! using DNS-based verification (CNAME or TXT records). use crate::error::{Error, Result}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; /// Verification method for custom domains #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum VerificationMethod { /// CNAME record pointing to name.synor.network Cname, /// TXT record with verification token Txt, } /// Domain verification status #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum VerificationStatus { /// Verification pending Pending, /// Domain verified Verified, /// Verification failed Failed, /// Verification expired (needs re-verification) Expired, } /// Domain record #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DomainRecord { /// The custom domain pub domain: String, /// Associated Synor name pub name: String, /// Verification method pub method: VerificationMethod, /// Verification token (for TXT method) pub token: String, /// Current status pub status: VerificationStatus, /// Block when verification was requested pub requested_at: u64, /// Block when verified (0 if not verified) pub verified_at: u64, /// Block when verification expires (0 = never) pub expires_at: u64, } /// Domain verifier pub struct DomainVerifier { /// Pending verifications pending: HashMap, /// Verified domains verified: HashMap, /// Synor hosting domain suffix hosting_domain: String, /// Verification token prefix token_prefix: String, /// Verification expiry in blocks verification_expiry: u64, } impl DomainVerifier { /// Create a new domain verifier pub fn new(hosting_domain: String) -> Self { Self { pending: HashMap::new(), verified: HashMap::new(), hosting_domain, token_prefix: "synor-verify=".to_string(), verification_expiry: 5_256_000, // ~1 year at 6s blocks } } /// Request verification for a domain pub fn request_verification( &mut self, domain: String, name: String, method: VerificationMethod, current_block: u64, ) -> Result { // Validate domain format self.validate_domain(&domain)?; // Check if already verified if self.verified.contains_key(&domain) { return Err(Error::DomainAlreadyLinked(domain)); } // Generate verification token let token = self.generate_token(&domain, &name, current_block); let record = DomainRecord { domain: domain.clone(), name, method, token, status: VerificationStatus::Pending, requested_at: current_block, verified_at: 0, expires_at: 0, }; self.pending.insert(domain, record.clone()); Ok(record) } /// Get verification instructions for a domain pub fn get_instructions(&self, domain: &str) -> Result { let record = self .pending .get(domain) .ok_or_else(|| Error::DomainNotFound(domain.to_string()))?; match record.method { VerificationMethod::Cname => Ok(VerificationInstructions { method: VerificationMethod::Cname, record_type: "CNAME".to_string(), record_name: record.domain.clone(), record_value: format!("{}.{}", record.name, self.hosting_domain), instructions: format!( "Add a CNAME record:\n {} CNAME {}.{}", record.domain, record.name, self.hosting_domain ), }), VerificationMethod::Txt => Ok(VerificationInstructions { method: VerificationMethod::Txt, record_type: "TXT".to_string(), record_name: format!("_synor.{}", record.domain), record_value: format!("{}{}", self.token_prefix, record.token), instructions: format!( "Add a TXT record:\n _synor.{} TXT \"{}{}\"", record.domain, self.token_prefix, record.token ), }), } } /// Verify a domain (check DNS records) pub fn verify(&mut self, domain: &str, current_block: u64) -> Result { let record = self .pending .get(domain) .ok_or_else(|| Error::DomainNotFound(domain.to_string()))? .clone(); // In production, this would do actual DNS lookups // For now, we'll simulate verification let verified = self.check_dns(&record)?; if verified { let mut verified_record = record; verified_record.status = VerificationStatus::Verified; verified_record.verified_at = current_block; verified_record.expires_at = current_block + self.verification_expiry; self.pending.remove(domain); self.verified .insert(domain.to_string(), verified_record.clone()); Ok(verified_record) } else { Err(Error::VerificationFailed( domain.to_string(), "DNS record not found or doesn't match".into(), )) } } /// Check if a domain is verified pub fn is_verified(&self, domain: &str, current_block: u64) -> bool { self.verified.get(domain).map_or(false, |r| { r.status == VerificationStatus::Verified && (r.expires_at == 0 || current_block <= r.expires_at) }) } /// Get verified domain record pub fn get_verified(&self, domain: &str) -> Option<&DomainRecord> { self.verified.get(domain) } /// Remove verification for a domain pub fn remove(&mut self, domain: &str) -> bool { self.pending.remove(domain).is_some() || self.verified.remove(domain).is_some() } /// Validate domain format fn validate_domain(&self, domain: &str) -> Result<()> { // Basic domain validation if domain.is_empty() { return Err(Error::InvalidDomain("Domain cannot be empty".into())); } if domain.len() > 253 { return Err(Error::InvalidDomain("Domain too long".into())); } // Check each label for label in domain.split('.') { if label.is_empty() || label.len() > 63 { return Err(Error::InvalidDomain("Invalid label length".into())); } if !label.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') { return Err(Error::InvalidDomain("Invalid characters in domain".into())); } if label.starts_with('-') || label.ends_with('-') { return Err(Error::InvalidDomain( "Label cannot start or end with hyphen".into(), )); } } // Must have at least one dot (not just a TLD) if !domain.contains('.') { return Err(Error::InvalidDomain( "Domain must have at least one dot".into(), )); } Ok(()) } /// Generate verification token fn generate_token(&self, domain: &str, name: &str, block: u64) -> String { let input = format!("{}:{}:{}", domain, name, block); let hash = blake3::hash(input.as_bytes()); hex::encode(&hash.as_bytes()[..16]) // 32 char hex token } /// Check DNS records (stub - real implementation would use DNS resolver) #[cfg(not(feature = "dns"))] fn check_dns(&self, _record: &DomainRecord) -> Result { // Without DNS feature, always return true (for testing) Ok(true) } /// Check DNS records (real implementation with trust-dns) #[cfg(feature = "dns")] fn check_dns(&self, record: &DomainRecord) -> Result { use trust_dns_resolver::config::*; use trust_dns_resolver::Resolver; let resolver = Resolver::new(ResolverConfig::default(), ResolverOpts::default()) .map_err(|e| Error::Dns(e.to_string()))?; match record.method { VerificationMethod::Cname => { let expected = format!("{}.{}.", record.name, self.hosting_domain); let response = resolver .lookup(&record.domain, RecordType::CNAME) .map_err(|e| Error::Dns(e.to_string()))?; for cname in response.iter() { if cname.to_string() == expected { return Ok(true); } } Ok(false) } VerificationMethod::Txt => { let txt_domain = format!("_synor.{}", record.domain); let expected = format!("{}{}", self.token_prefix, record.token); let response = resolver .txt_lookup(&txt_domain) .map_err(|e| Error::Dns(e.to_string()))?; for txt in response.iter() { let txt_str = txt.to_string(); if txt_str.contains(&expected) { return Ok(true); } } Ok(false) } } } } /// Verification instructions for users #[derive(Debug, Clone, Serialize, Deserialize)] pub struct VerificationInstructions { /// Verification method pub method: VerificationMethod, /// DNS record type to add pub record_type: String, /// DNS record name pub record_name: String, /// DNS record value pub record_value: String, /// Human-readable instructions pub instructions: String, } #[cfg(test)] mod tests { use super::*; #[test] fn test_request_verification() { let mut verifier = DomainVerifier::new("synor.network".to_string()); let record = verifier .request_verification( "myapp.com".to_string(), "myapp".to_string(), VerificationMethod::Cname, 0, ) .unwrap(); assert_eq!(record.domain, "myapp.com"); assert_eq!(record.name, "myapp"); assert_eq!(record.status, VerificationStatus::Pending); } #[test] fn test_get_instructions() { let mut verifier = DomainVerifier::new("synor.network".to_string()); verifier .request_verification( "myapp.com".to_string(), "myapp".to_string(), VerificationMethod::Cname, 0, ) .unwrap(); let instructions = verifier.get_instructions("myapp.com").unwrap(); assert_eq!(instructions.record_type, "CNAME"); assert!(instructions.record_value.contains("myapp.synor.network")); } #[test] fn test_verify_domain() { let mut verifier = DomainVerifier::new("synor.network".to_string()); verifier .request_verification( "myapp.com".to_string(), "myapp".to_string(), VerificationMethod::Cname, 0, ) .unwrap(); // Without DNS feature, verify always succeeds let record = verifier.verify("myapp.com", 100).unwrap(); assert_eq!(record.status, VerificationStatus::Verified); assert!(verifier.is_verified("myapp.com", 100)); } #[test] fn test_invalid_domain() { let mut verifier = DomainVerifier::new("synor.network".to_string()); // No TLD assert!(verifier .request_verification( "myapp".to_string(), "myapp".to_string(), VerificationMethod::Cname, 0 ) .is_err()); // Empty assert!(verifier .request_verification( "".to_string(), "myapp".to_string(), VerificationMethod::Cname, 0 ) .is_err()); } }