1pub mod whois;
32pub mod rdap;
33pub mod cache;
34pub mod config;
35pub mod errors;
36pub mod tld_mappings;
37pub mod buffer_pool;
38pub mod parser;
39pub mod tld;
40pub mod dates;
41pub mod rate_limiter;
42pub mod ip;
43
44
45pub use whois::{WhoisService, WhoisResult};
47pub use rdap::{RdapService, RdapResult};
48pub use cache::CacheService;
49pub use config::Config;
50pub use errors::WhoisError;
51pub use tld::extract_tld;
52pub use dates::{parse_date, calculate_date_fields};
53pub use ip::{ValidatedIpAddress, Rir, detect_rir};
54
55use std::sync::Arc;
56
57#[derive(Debug, Clone)]
67pub struct ValidatedDomain(pub String);
68
69impl ValidatedDomain {
70 pub fn new(domain: impl Into<String>) -> Result<Self, WhoisError> {
77 use addr::parser::DnsName;
78 use addr::psl::List;
79
80 let domain = domain.into().trim().to_lowercase();
81
82 if domain.is_empty() {
84 return Err(WhoisError::InvalidDomain("Empty domain".to_string()));
85 }
86
87 if !domain.contains('.') {
89 return Err(WhoisError::InvalidDomain("Domain must contain at least one dot".to_string()));
90 }
91
92 List.parse_dns_name(&domain)
95 .map_err(|e| WhoisError::InvalidDomain(format!("Invalid domain: {}", e)))?;
96
97 Ok(ValidatedDomain(domain))
98 }
99
100 pub fn as_str(&self) -> &str {
102 &self.0
103 }
104
105 pub fn into_inner(self) -> String {
107 self.0
108 }
109}
110
111impl AsRef<str> for ValidatedDomain {
112 fn as_ref(&self) -> &str {
113 &self.0
114 }
115}
116
117impl std::fmt::Display for ValidatedDomain {
118 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119 write!(f, "{}", self.0)
120 }
121}
122
123#[derive(Debug, Clone)]
128pub enum DetectedQueryType {
129 Domain(ValidatedDomain),
131 IpAddress(ValidatedIpAddress),
133}
134
135#[derive(Debug, Clone)]
158pub struct ValidatedQuery {
159 query_type: DetectedQueryType,
160 original: String,
161}
162
163impl ValidatedQuery {
164 pub fn new(input: impl Into<String>) -> Result<Self, WhoisError> {
173 let input = input.into();
174 let trimmed = input.trim();
175 let original = input.clone();
176
177 if let Ok(ip) = ValidatedIpAddress::new(trimmed) {
179 return Ok(Self {
180 query_type: DetectedQueryType::IpAddress(ip),
181 original,
182 });
183 }
184
185 let domain = ValidatedDomain::new(trimmed)?;
187 Ok(Self {
188 query_type: DetectedQueryType::Domain(domain),
189 original,
190 })
191 }
192
193 pub fn query_type(&self) -> &DetectedQueryType {
195 &self.query_type
196 }
197
198 pub fn as_str(&self) -> &str {
200 match &self.query_type {
201 DetectedQueryType::Domain(d) => d.as_str(),
202 DetectedQueryType::IpAddress(ip) => ip.as_str(),
203 }
204 }
205
206 pub fn is_domain(&self) -> bool {
208 matches!(self.query_type, DetectedQueryType::Domain(_))
209 }
210
211 pub fn is_ip(&self) -> bool {
213 matches!(self.query_type, DetectedQueryType::IpAddress(_))
214 }
215
216 pub fn into_inner(self) -> String {
218 match self.query_type {
219 DetectedQueryType::Domain(d) => d.into_inner(),
220 DetectedQueryType::IpAddress(ip) => ip.into_inner(),
221 }
222 }
223}
224
225impl AsRef<str> for ValidatedQuery {
226 fn as_ref(&self) -> &str {
227 self.as_str()
228 }
229}
230
231impl std::fmt::Display for ValidatedQuery {
232 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
233 write!(f, "{}", self.as_str())
234 }
235}
236
237#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
239#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
240pub struct ParsedWhoisData {
241 #[cfg_attr(feature = "openapi", schema(example = "MarkMonitor Inc."))]
243 pub registrar: Option<String>,
244
245 #[cfg_attr(feature = "openapi", schema(example = "1997-09-15T04:00:00Z"))]
247 pub creation_date: Option<String>,
248
249 #[cfg_attr(feature = "openapi", schema(example = "2028-09-14T04:00:00Z"))]
251 pub expiration_date: Option<String>,
252
253 #[cfg_attr(feature = "openapi", schema(example = "2019-09-09T15:39:04Z"))]
255 pub updated_date: Option<String>,
256
257 #[cfg_attr(feature = "openapi", schema(example = json!(["NS1.GOOGLE.COM", "NS2.GOOGLE.COM"])))]
259 pub name_servers: Vec<String>,
260
261 #[cfg_attr(feature = "openapi", schema(example = json!(["clientDeleteProhibited", "clientTransferProhibited"])))]
263 pub status: Vec<String>,
264
265 pub registrant_name: Option<String>,
267
268 pub registrant_email: Option<String>,
270
271 pub admin_email: Option<String>,
273
274 pub tech_email: Option<String>,
276
277 #[cfg_attr(feature = "openapi", schema(example = 10117))]
279 pub created_ago: Option<i64>,
280
281 #[cfg_attr(feature = "openapi", schema(example = 45))]
283 pub updated_ago: Option<i64>,
284
285 #[cfg_attr(feature = "openapi", schema(example = 1204))]
287 pub expires_in: Option<i64>,
288}
289
290impl ParsedWhoisData {
291 pub fn new() -> Self {
296 Self {
297 registrar: None,
298 creation_date: None,
299 expiration_date: None,
300 updated_date: None,
301 name_servers: Vec::new(),
302 status: Vec::new(),
303 registrant_name: None,
304 registrant_email: None,
305 admin_email: None,
306 tech_email: None,
307 created_ago: None,
308 updated_ago: None,
309 expires_in: None,
310 }
311 }
312
313 pub fn calculate_age_fields(&mut self) {
318 let (created_ago, updated_ago, expires_in) = dates::calculate_date_fields(
319 &self.creation_date,
320 &self.updated_date,
321 &self.expiration_date,
322 );
323 self.created_ago = created_ago;
324 self.updated_ago = updated_ago;
325 self.expires_in = expires_in;
326 }
327}
328
329#[derive(Debug, Clone)]
334pub struct LookupResult {
335 pub server: String,
337 pub raw_data: String,
339 pub parsed_data: Option<ParsedWhoisData>,
341 pub parsing_analysis: Vec<String>,
343}
344
345#[derive(Clone)]
347pub struct WhoisClient {
348 service: Arc<WhoisService>,
349 cache: Option<Arc<CacheService>>,
350}
351
352impl WhoisClient {
353 pub async fn new() -> Result<Self, WhoisError> {
357 let config = Self::load_default_config()?;
358 Self::new_with_config(config).await
359 }
360
361 pub async fn new_with_config(config: Arc<Config>) -> Result<Self, WhoisError> {
363 let service = Arc::new(WhoisService::new(config.clone()).await?);
364 let cache = Self::initialize_cache(config);
365
366 Ok(Self { service, cache })
367 }
368
369 pub async fn new_without_cache() -> Result<Self, WhoisError> {
371 let config = Self::load_default_config()?;
372 let service = Arc::new(WhoisService::new(config).await?);
373
374 Ok(Self { service, cache: None })
375 }
376
377 fn initialize_cache(config: Arc<Config>) -> Option<Arc<CacheService>> {
379 Some(Arc::new(CacheService::new(config)))
380 }
381
382 pub async fn lookup(&self, query: &str) -> Result<WhoisResponse, WhoisError> {
408 self.lookup_with_options(query, false).await
409 }
410
411 pub async fn lookup_fresh(&self, query: &str) -> Result<WhoisResponse, WhoisError> {
413 self.lookup_with_options(query, true).await
414 }
415
416 pub async fn lookup_with_options(&self, query: &str, fresh: bool) -> Result<WhoisResponse, WhoisError> {
420 let start_time = std::time::Instant::now();
421
422 let validated = ValidatedQuery::new(query)?;
424
425 let is_domain = validated.is_domain();
427 let query_str = validated.as_str().to_string();
428 let original = validated.into_inner();
429
430 if is_domain {
432 self.lookup_domain_internal(&query_str, fresh, start_time, original).await
433 } else {
434 self.lookup_ip_internal(&query_str, fresh, start_time, original).await
435 }
436 }
437
438 async fn lookup_internal(
443 &self,
444 query: &str,
445 is_ip: bool,
446 fresh: bool,
447 start_time: std::time::Instant,
448 original: String,
449 ) -> Result<WhoisResponse, WhoisError> {
450 if fresh {
452 let result = if is_ip {
453 self.service.lookup_ip(query).await?
454 } else {
455 self.service.lookup(query).await?
456 };
457 let query_time = start_time.elapsed().as_millis() as u64;
458
459 return Ok(WhoisResponse {
460 domain: original,
461 whois_server: result.server,
462 raw_data: result.raw_data,
463 parsed_data: result.parsed_data,
464 cached: false,
465 query_time_ms: query_time,
466 parsing_analysis: None,
467 });
468 }
469
470 if let Some(cache) = &self.cache {
472 let query_owned = query.to_string();
473 let service = self.service.clone();
474
475 let mut response = cache
476 .get_or_fetch(query, || async move {
477 let result = if is_ip {
478 service.lookup_ip(&query_owned).await?
479 } else {
480 service.lookup(&query_owned).await?
481 };
482 let query_time = start_time.elapsed().as_millis() as u64;
483
484 Ok(WhoisResponse {
485 domain: query_owned.clone(),
486 whois_server: result.server,
487 raw_data: result.raw_data,
488 parsed_data: result.parsed_data,
489 cached: false,
490 query_time_ms: query_time,
491 parsing_analysis: None,
492 })
493 })
494 .await?;
495
496 response.domain = original;
498 Ok(response)
499 } else {
500 let result = if is_ip {
502 self.service.lookup_ip(query).await?
503 } else {
504 self.service.lookup(query).await?
505 };
506 let query_time = start_time.elapsed().as_millis() as u64;
507
508 Ok(WhoisResponse {
509 domain: original,
510 whois_server: result.server,
511 raw_data: result.raw_data,
512 parsed_data: result.parsed_data,
513 cached: false,
514 query_time_ms: query_time,
515 parsing_analysis: None,
516 })
517 }
518 }
519
520 async fn lookup_domain_internal(
522 &self,
523 domain: &str,
524 fresh: bool,
525 start_time: std::time::Instant,
526 original: String,
527 ) -> Result<WhoisResponse, WhoisError> {
528 self.lookup_internal(domain, false, fresh, start_time, original).await
529 }
530
531 async fn lookup_ip_internal(
533 &self,
534 ip_addr: &str,
535 fresh: bool,
536 start_time: std::time::Instant,
537 original: String,
538 ) -> Result<WhoisResponse, WhoisError> {
539 self.lookup_internal(ip_addr, true, fresh, start_time, original).await
540 }
541
542
543 pub fn cache_enabled(&self) -> bool {
547 self.cache.is_some()
548 }
549
550 fn load_default_config() -> Result<Arc<Config>, WhoisError> {
554 let config = Arc::new(Config::load().map_err(WhoisError::ConfigError)?);
555 Ok(config)
556 }
557}
558
559#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
561#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
562pub struct WhoisResponse {
563 pub domain: String,
564 pub whois_server: String,
565 pub raw_data: String,
566 pub parsed_data: Option<ParsedWhoisData>,
567 pub cached: bool,
568 pub query_time_ms: u64,
569 #[serde(skip_serializing_if = "Option::is_none")]
570 pub parsing_analysis: Option<Vec<String>>,
571}
572
573#[cfg(test)]
574mod tests {
575 use super::*;
576
577 #[tokio::test]
578 async fn test_whois_client_creation() {
579 let client = WhoisClient::new_without_cache().await;
580 assert!(client.is_ok());
581 }
582
583 #[tokio::test]
584 async fn test_domain_validation() {
585 let client = WhoisClient::new_without_cache().await.unwrap();
586
587 let result = client.lookup("").await;
589 assert!(matches!(result, Err(WhoisError::InvalidDomain(_))));
590
591 let result = client.lookup("invalid").await;
593 assert!(matches!(result, Err(WhoisError::InvalidDomain(_))));
594 }
595
596 #[test]
597 fn test_validated_domain_valid() {
598 assert!(ValidatedDomain::new("example.com").is_ok());
600 assert!(ValidatedDomain::new("sub.example.com").is_ok());
601 assert!(ValidatedDomain::new("deep.sub.example.com").is_ok());
602
603 assert!(ValidatedDomain::new("EXAMPLE.COM").is_ok());
605 assert!(ValidatedDomain::new("Example.Com").is_ok());
606
607 assert!(ValidatedDomain::new(" example.com ").is_ok());
609
610 assert!(ValidatedDomain::new("example.co.uk").is_ok());
612 assert!(ValidatedDomain::new("example.com.au").is_ok());
613
614 assert!(ValidatedDomain::new("my-site.example.com").is_ok());
616 assert!(ValidatedDomain::new("a-b-c.example.com").is_ok());
617 }
618
619 #[test]
620 fn test_validated_domain_invalid() {
621 assert!(ValidatedDomain::new("").is_err());
623 assert!(ValidatedDomain::new(" ").is_err());
624
625 assert!(ValidatedDomain::new("com").is_err());
627 assert!(ValidatedDomain::new("localhost").is_err());
628
629 assert!(ValidatedDomain::new("example..com").is_err());
631
632 }
639
640 #[test]
641 fn test_validated_domain_normalization() {
642 let domain = ValidatedDomain::new("EXAMPLE.COM").unwrap();
644 assert_eq!(domain.as_str(), "example.com");
645
646 let domain = ValidatedDomain::new(" example.com ").unwrap();
648 assert_eq!(domain.as_str(), "example.com");
649
650 let domain = ValidatedDomain::new("Example.Com").unwrap();
652 assert_eq!(domain.as_str(), "example.com");
653 }
654
655 #[test]
656 fn test_validated_domain_edge_cases() {
657 assert!(ValidatedDomain::new("a.b.c").is_ok());
659
660 assert!(ValidatedDomain::new("123.456.com").is_ok());
662
663 assert!(ValidatedDomain::new("123.456").is_ok());
665
666 let max_label = "a".repeat(63);
668 assert!(ValidatedDomain::new(format!("{}.com", max_label)).is_ok());
669
670 let valid_long = format!("{}.{}.{}.com", "a".repeat(50), "b".repeat(50), "c".repeat(50));
672 assert!(ValidatedDomain::new(valid_long).is_ok());
673 }
674
675 #[test]
676 fn test_validated_domain_methods() {
677 let domain = ValidatedDomain::new("example.com").unwrap();
678
679 assert_eq!(domain.as_str(), "example.com");
681
682 let s: &str = domain.as_ref();
684 assert_eq!(s, "example.com");
685
686 assert_eq!(format!("{}", domain), "example.com");
688
689 let domain2 = ValidatedDomain::new("test.com").unwrap();
691 let inner = domain2.into_inner();
692 assert_eq!(inner, "test.com");
693 }
694}