387 lines
12 KiB
Rust
387 lines
12 KiB
Rust
//! 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<String, DomainRecord>,
|
|
/// Verified domains
|
|
verified: HashMap<String, DomainRecord>,
|
|
/// 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<DomainRecord> {
|
|
// 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<VerificationInstructions> {
|
|
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<DomainRecord> {
|
|
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<bool> {
|
|
// 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<bool> {
|
|
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());
|
|
}
|
|
}
|