ezwhois_rs/
lib.rs

1#![crate_type = "lib"]
2//! Whois information parsing and querying crate. Provides a high level API.
3//!
4//! Enable the 'parser' flag if you want to use the parser.
5//! Everything related to the parser can be found at [parser]
6//!
7#![doc = include_str!("../README.md")]
8
9use tokio::io::{AsyncReadExt, AsyncWriteExt};
10use tokio::net::TcpStream;
11use std::future::Future;
12
13#[cfg(feature = "parser")]
14pub mod parser;
15
16#[derive(Clone)]
17/// Configuration for your WHOIS instance
18pub struct WhoisOpt {
19    pub whois_server: &'static str,
20    pub domain2lookup: &'static str
21}
22
23#[derive(Clone)]
24/// Whois instance, used for querying a domain to a specific WHOIS server for WHOIS data
25/// ```
26/// use ezwhois_rs::{
27///     parser::Parser,
28///     Whois,
29///     WhoisOpt,
30///     WhoisResolver
31/// };
32/// 
33/// #[tokio::main]
34/// async fn main() {
35///     let client = Whois::new(WhoisOpt{
36///         whois_server: "whois.iana.org:43", 
37///         domain2lookup: "simpaix.net"
38///     });
39///     let res = client.query().await.expect("expected a response");
40///
41///     let parser = Parser::new();
42///     let info = parser.parse(res).unwrap();
43///     println!("creation date: {}\nexpire: {}", 
44///         info.creation_date.unwrap().format("%d/%m/%Y %H:%M"),
45///         info.registry_expirity_date.unwrap().format("%d/%m/%Y %H:%M")
46///     ); // info.registry_domain_id , etc etc
47/// }
48/// ```
49pub struct Whois{
50    target: WhoisOpt
51}
52
53pub trait WhoisResolver: Sized {
54    type Error;
55
56    /// Creates a new whois instance and configures the target
57    fn new(opt: WhoisOpt) -> Whois;
58    
59    /// Queries the WHOIS server and retrieves domain information.
60    /// Returns WHOIS information as a string.
61    ///
62    /// So that you can use any arbitrary parser.
63    fn query(&self) -> impl Future<Output = Result<String, Self::Error>>;
64}
65
66impl WhoisResolver for Whois {
67    type Error = Box<dyn std::error::Error>;
68
69    fn new(opt: WhoisOpt) -> Whois {
70        Whois{target: opt}
71    }
72    
73    async fn query(&self) -> Result<String, Self::Error> {
74        let q1 = Whois::lookup(self.target.whois_server, self.target.domain2lookup).await?;
75        let main_server = 
76        if let Some((_, b)) = q1.split_once("whois:") {
77            b.trim().split_once("\n").ok_or_else(|| errors::WhoisError::MissingNewline)?.0
78        } else { return Err(Box::new(errors::WhoisError::GeneralErr { ctx: "could not find whois server to lookup" }));};
79
80        let port: &str = self.target.whois_server.split_once(":").ok_or_else(|| {
81            Box::new(errors::WhoisError::GeneralErr{ ctx: "whois server should be in host:port format" })
82        })?.1;
83        Ok(Whois::lookup(&format!("{main_server}:{port}"), self.target.domain2lookup).await?)
84    }
85}
86
87impl Whois {
88    /// private!
89    /// Sends a query request to the WHOIS server and returns a String that holds WHOIS information
90    async fn lookup(whois_server: &str, domain2_lookup: &str) -> Result<String, Box<dyn std::error::Error>> {
91        let mut conn = TcpStream::connect(whois_server).await?;
92        conn.write(format!("{domain2_lookup}\r\n").as_bytes()).await?;
93    
94        let mut data: Vec<u8> = vec![];
95        conn.read_to_end(&mut data).await?;
96        
97        if data.len() == 0 {
98            return Err(Box::new(errors::WhoisError::WhoisServerIO { ctx: "Wrote to WHOIS server, but got no response" }));
99        }
100        Ok(String::from_utf8(data)?)
101    }
102}
103
104// Errors that may occur for parent module
105pub mod errors {
106    use thiserror::Error;
107
108    #[derive(Error, Debug)]
109    pub enum WhoisError {
110        #[error("Error caused by I/O on the WHOIS server: {ctx}")]
111        WhoisServerIO{ctx: &'static str},
112        
113        #[error("error: {ctx}")]
114        GeneralErr{ctx: &'static str},
115
116        #[error("couldn't find newline seperator")]
117        MissingNewline
118    }
119}
120
121#[tokio::test]
122async fn test_client() {
123    let client = Whois::new(WhoisOpt{
124        whois_server: "whois.iana.org:43", 
125        domain2lookup: "simpaix.net"
126    });
127    let res = client.query().await.expect("expected a response");
128
129    let parser = parser::Parser::new();
130    let info = parser.parse(res).unwrap();
131    println!("creation date: {}\nexpire: {}\n\nor the complete whois info:\n\n-------------{:#?}------------", 
132        info.creation_date.unwrap().format("%d/%m/%Y %H:%M"),
133        info.registry_expirity_date.unwrap().format("%d/%m/%Y %H:%M"),
134        info
135    );
136}
137/*
138successes:
139
140---- test_client stdout ----
141creation date: 20/06/2023 12:13
142expire: 20/06/2025 12:13
143
144or the complete whois info:
145
146-------------WhoisInformation {
147    domain_name: Some(
148        "SIMPAIX.NET",
149    ),
150    registry_domain_id: Some(
151        "2791830160_DOMAIN_NET-VRSN",
152    ),
153    registrar_whois_server: Some(
154        "whois.namecheap.com",
155    ),
156    registrar_url: Some(
157        "http://www.namecheap.com",
158    ),
159    updated_date: Some(
160        2024-05-21T08:07:15Z,
161    ),
162    creation_date: Some(
163        2023-06-20T12:13:22Z,
164    ),
165    registry_expirity_date: Some(
166        2025-06-20T12:13:22Z,
167    ),
168    registrar: Some(
169        "NameCheap, Inc.",
170    ),
171    registrar_iana_id: Some(
172        "1068",
173    ),
174    registrar_abuse_email_contact: Some(
175        "abuse@namecheap.com",
176    ),
177    registrar_abuse_phone_contact: Some(
178        "+1.6613102107",
179    ),
180    domain_status: Some(
181        "clientTransferProhibited https://icann.org/epp#clientTransferProhibited",
182    ),
183    name_servers: Some(
184        [
185            "IRENA.NS.CLOUDFLARE.COM",
186        ],
187    ),
188    dnssec: Some(
189        "unsigned",
190    ),
191}------------
192*/