use std::time::Duration; #[cfg(feature = "rustls")] use rustls_pki_types::{CertificateDer, PrivateKeyDer}; use tracing::{debug, error}; use crate::common::NoExtension; pub use crate::connection::Connector; use crate::connection::EppConnection; use crate::error::Error; use crate::hello::{Greeting, Hello}; use crate::request::{Command, CommandWrapper, Extension, Transaction}; use crate::response::{Response, ResponseStatus}; use crate::xml; /// An `EppClient` provides an interface to sending EPP requests to a registry /// /// Once initialized, the EppClient instance can serialize EPP requests to XML and send them /// to the registry and deserialize the XML responses from the registry to local types. /// /// # Examples /// /// ```no_run /// # use std::collections::HashMap; /// # use std::net::ToSocketAddrs; /// # use std::time::Duration; /// # /// use instant_epp::EppClient; /// use instant_epp::domain::DomainCheck; /// use instant_epp::common::NoExtension; /// /// # #[cfg(feature = "rustls")] /// # #[tokio::main] /// # async fn main() { /// // Create an instance of EppClient /// let timeout = Duration::from_secs(5); /// let mut client = match EppClient::connect("registry_name".to_string(), ("example.com".to_owned(), 7000), None, timeout).await { /// Ok(client) => client, /// Err(e) => panic!("Failed to create EppClient: {}", e) /// }; /// /// // Make a EPP Hello call to the registry /// let greeting = client.hello().await.unwrap(); /// println!("{:?}", greeting); /// /// // Execute an EPP Command against the registry with distinct request and response objects /// let domain_check = DomainCheck { domains: &["eppdev.com", "eppdev.net"] }; /// let response = client.transact(&domain_check, "transaction-id").await.unwrap(); /// response /// .res_data() /// .unwrap() /// .list /// .iter() /// .for_each(|chk| println!("Domain: {}, Available: {}", chk.inner.id, chk.inner.available)); /// # } /// # /// # #[cfg(not(feature = "rustls"))] /// # fn main() {} /// ``` /// /// The output would look like this: /// /// ```text /// Domain: eppdev.com, Available: 1 /// Domain: eppdev.net, Available: 1 /// ``` pub struct EppClient { connection: EppConnection, } #[cfg(feature = "rustls")] impl EppClient { /// Connect to the specified `addr` and `hostname` over TLS /// /// The `registry` is used as a name in internal logging; `host` provides the host name /// and port to connect to), `hostname` is sent as the TLS server name indication and /// `identity` provides optional TLS client authentication (using) rustls as the TLS /// implementation. The `timeout` limits the time spent on any underlying network operations. /// /// Alternatively, use `EppClient::new()` with any established `AsyncRead + AsyncWrite + Unpin` /// implementation. pub async fn connect( registry: String, server: (String, u16), identity: Option<(Vec>, PrivateKeyDer<'static>)>, timeout: Duration, ) -> Result { let connector = RustlsConnector::new(server, identity).await?; Self::new(connector, registry, timeout).await } } impl EppClient { /// Create an `EppClient` from an already established connection pub async fn new(connector: C, registry: String, timeout: Duration) -> Result { Ok(Self { connection: EppConnection::new(connector, registry, timeout).await?, }) } /// Executes an EPP Hello call and returns the response as a `Greeting` pub async fn hello(&mut self) -> Result { let xml = xml::serialize(Hello)?; debug!("{}: hello: {}", self.connection.registry, &xml); let response = self.connection.transact(&xml)?.await?; debug!("{}: greeting: {}", self.connection.registry, &response); xml::deserialize::(&response) } pub async fn transact<'c, 'e, Cmd, Ext>( &mut self, data: impl Into>, id: &str, ) -> Result, Error> where Cmd: Transaction + Command + 'c, Ext: Extension + 'e, { let data = data.into(); let document = CommandWrapper::new(data.command, data.extension, id); let xml = xml::serialize(&document)?; debug!("{}: request: {}", self.connection.registry, &xml); let response = self.connection.transact(&xml)?.await?; debug!("{}: response: {}", self.connection.registry, &response); let rsp = match xml::deserialize::>(&response) { Ok(rsp) => rsp, Err(e) => { error!(%response, "failed to deserialize response for transaction: {e}"); return Err(e); } }; if rsp.result.code.is_success() { return Ok(rsp); } let err = crate::error::Error::Command(Box::new(ResponseStatus { result: rsp.result, tr_ids: rsp.tr_ids, })); Err(err) } /// Accepts raw EPP XML and returns the raw EPP XML response to it. /// Not recommended for direct use but sometimes can be useful for debugging pub async fn transact_xml(&mut self, xml: &str) -> Result { self.connection.transact(xml)?.await } /// Returns the greeting received on establishment of the connection in raw xml form pub fn xml_greeting(&self) -> String { String::from(&self.connection.greeting) } /// Returns the greeting received on establishment of the connection as an `Greeting` pub fn greeting(&self) -> Result { xml::deserialize::(&self.connection.greeting) } pub async fn reconnect(&mut self) -> Result<(), Error> { self.connection.reconnect().await } pub async fn shutdown(mut self) -> Result<(), Error> { self.connection.shutdown().await } } #[derive(Debug)] pub struct RequestData<'c, 'e, C, E> { pub(crate) command: &'c C, pub(crate) extension: Option<&'e E>, } impl<'c, C: Command> From<&'c C> for RequestData<'c, 'static, C, NoExtension> { fn from(command: &'c C) -> Self { Self { command, extension: None, } } } impl<'c, 'e, C: Command, E: Extension> From<(&'c C, &'e E)> for RequestData<'c, 'e, C, E> { fn from((command, extension): (&'c C, &'e E)) -> Self { Self { command, extension: Some(extension), } } } // Manual impl because this does not depend on whether `C` and `E` are `Clone` impl<'c, 'e, C, E> Clone for RequestData<'c, 'e, C, E> { fn clone(&self) -> Self { *self } } // Manual impl because this does not depend on whether `C` and `E` are `Copy` impl<'c, 'e, C, E> Copy for RequestData<'c, 'e, C, E> {} #[cfg(feature = "rustls")] pub use rustls_connector::RustlsConnector; #[cfg(feature = "rustls")] mod rustls_connector { use std::io; use std::sync::Arc; use std::time::Duration; use async_trait::async_trait; use rustls_pki_types::{CertificateDer, PrivateKeyDer, ServerName}; use tokio::net::lookup_host; use tokio::net::TcpStream; use tokio_rustls::client::TlsStream; use tokio_rustls::rustls::{ClientConfig, RootCertStore}; use tokio_rustls::TlsConnector; use tracing::info; use crate::connection::{self, Connector}; use crate::error::Error; pub struct RustlsConnector { inner: TlsConnector, domain: ServerName<'static>, server: (String, u16), } impl RustlsConnector { pub async fn new( server: (String, u16), identity: Option<(Vec>, PrivateKeyDer<'static>)>, ) -> Result { let mut roots = RootCertStore::empty(); for cert in rustls_native_certs::load_native_certs()? { roots.add(cert).map_err(|err| { Box::new(err) as Box })?; } let builder = ClientConfig::builder().with_root_certificates(roots); let config = match identity { Some((certs, key)) => builder .with_client_auth_cert(certs, key) .map_err(|e| Error::Other(e.into()))?, None => builder.with_no_client_auth(), }; let domain = ServerName::try_from(server.0.as_str()) .map_err(|_| { io::Error::new( io::ErrorKind::InvalidInput, format!("invalid domain: {}", server.0), ) })? .to_owned(); Ok(Self { inner: TlsConnector::from(Arc::new(config)), domain, server, }) } } #[async_trait] impl Connector for RustlsConnector { type Connection = TlsStream; async fn connect(&self, timeout: Duration) -> Result { info!("Connecting to server: {}:{}", self.server.0, self.server.1); let addr = match lookup_host(&self.server).await?.next() { Some(addr) => addr, None => { return Err(Error::Io(io::Error::new( io::ErrorKind::InvalidInput, format!("Invalid host: {}", &self.server.0), ))) } }; let stream = TcpStream::connect(addr).await?; let future = self.inner.connect(self.domain.clone(), stream); connection::timeout(timeout, future).await } } }