Split client and connection into two halfes
This rewrites the logic to process requests to follow the I/O task pattern. This makes it easier to implement things like keepalives as well as dealing with dropped futures.
This commit is contained in:
parent
eb2ff138f5
commit
065210b8e8
|
@ -13,9 +13,9 @@ default = ["tokio-rustls"]
|
||||||
async-trait = "0.1.52"
|
async-trait = "0.1.52"
|
||||||
celes = "2.1"
|
celes = "2.1"
|
||||||
chrono = { version = "0.4.23", features = ["serde"] }
|
chrono = { version = "0.4.23", features = ["serde"] }
|
||||||
quick-xml = { version = "0.26", features = [ "serialize" ] }
|
quick-xml = { version = "0.26", features = ["serialize"] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
tokio = { version = "1.0", features = [ "full" ] }
|
tokio = { version = "1.0", features = ["full"] }
|
||||||
tokio-rustls = { version = "0.23", optional = true }
|
tokio-rustls = { version = "0.23", optional = true }
|
||||||
tracing = "0.1.29"
|
tracing = "0.1.29"
|
||||||
webpki-roots = "0.22.1"
|
webpki-roots = "0.22.1"
|
||||||
|
|
139
src/client.rs
139
src/client.rs
|
@ -1,10 +1,15 @@
|
||||||
use std::time::Duration;
|
use std::borrow::Cow;
|
||||||
|
use std::future::Future;
|
||||||
|
use std::pin::Pin;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::task::{Context, Poll};
|
||||||
|
|
||||||
|
use tokio::sync::mpsc;
|
||||||
use tracing::{debug, error};
|
use tracing::{debug, error};
|
||||||
|
|
||||||
use crate::common::NoExtension;
|
use crate::common::NoExtension;
|
||||||
pub use crate::connect::Connector;
|
pub use crate::connect::Connector;
|
||||||
use crate::connection::EppConnection;
|
use crate::connection::{Request, RequestMessage};
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
use crate::hello::{Greeting, GreetingDocument, HelloDocument};
|
use crate::hello::{Greeting, GreetingDocument, HelloDocument};
|
||||||
use crate::request::{Command, CommandDocument, Extension, Transaction};
|
use crate::request::{Command, CommandDocument, Extension, Transaction};
|
||||||
|
@ -13,8 +18,9 @@ use crate::xml;
|
||||||
|
|
||||||
/// An `EppClient` provides an interface to sending EPP requests to a registry
|
/// 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
|
/// Once initialized, the [`EppClient`] instance is the API half that is returned when creating a new connection.
|
||||||
/// to the registry and deserialize the XML responses from the registry to local types.
|
/// It can serialize EPP requests to XML and send them to the registry and deserialize the XML responses from the
|
||||||
|
/// registry to local types.
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
|
@ -23,7 +29,7 @@ use crate::xml;
|
||||||
/// # use std::net::ToSocketAddrs;
|
/// # use std::net::ToSocketAddrs;
|
||||||
/// # use std::time::Duration;
|
/// # use std::time::Duration;
|
||||||
/// #
|
/// #
|
||||||
/// use epp_client::EppClient;
|
/// use epp_client::connect::connect;
|
||||||
/// use epp_client::domain::DomainCheck;
|
/// use epp_client::domain::DomainCheck;
|
||||||
/// use epp_client::common::NoExtension;
|
/// use epp_client::common::NoExtension;
|
||||||
///
|
///
|
||||||
|
@ -31,11 +37,15 @@ use crate::xml;
|
||||||
/// # async fn main() {
|
/// # async fn main() {
|
||||||
/// // Create an instance of EppClient
|
/// // Create an instance of EppClient
|
||||||
/// let timeout = Duration::from_secs(5);
|
/// let timeout = Duration::from_secs(5);
|
||||||
/// let mut client = match EppClient::connect("registry_name".to_string(), ("example.com".to_owned(), 7000), None, timeout).await {
|
/// let (mut client, mut connection) = match connect("registry_name".into(), ("example.com".into(), 7000), None, timeout).await {
|
||||||
/// Ok(client) => client,
|
/// Ok(client) => client,
|
||||||
/// Err(e) => panic!("Failed to create EppClient: {}", e)
|
/// Err(e) => panic!("Failed to create EppClient: {}", e)
|
||||||
/// };
|
/// };
|
||||||
///
|
///
|
||||||
|
/// tokio::spawn(async move {
|
||||||
|
/// connection.run().await.unwrap();
|
||||||
|
/// });
|
||||||
|
///
|
||||||
/// // Make a EPP Hello call to the registry
|
/// // Make a EPP Hello call to the registry
|
||||||
/// let greeting = client.hello().await.unwrap();
|
/// let greeting = client.hello().await.unwrap();
|
||||||
/// println!("{:?}", greeting);
|
/// println!("{:?}", greeting);
|
||||||
|
@ -55,26 +65,26 @@ use crate::xml;
|
||||||
/// Domain: eppdev.com, Available: 1
|
/// Domain: eppdev.com, Available: 1
|
||||||
/// Domain: eppdev.net, Available: 1
|
/// Domain: eppdev.net, Available: 1
|
||||||
/// ```
|
/// ```
|
||||||
pub struct EppClient<C: Connector> {
|
pub struct EppClient {
|
||||||
connection: EppConnection<C>,
|
inner: Arc<InnerClient>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<C: Connector> EppClient<C> {
|
impl EppClient {
|
||||||
/// Create an `EppClient` from an already established connection
|
/// Create an `EppClient` from an already established connection
|
||||||
pub async fn new(connector: C, registry: String, timeout: Duration) -> Result<Self, Error> {
|
pub(crate) fn new(sender: mpsc::UnboundedSender<Request>, registry: Cow<'static, str>) -> Self {
|
||||||
Ok(Self {
|
Self {
|
||||||
connection: EppConnection::new(connector, registry, timeout).await?,
|
inner: Arc::new(InnerClient { sender, registry }),
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Executes an EPP Hello call and returns the response as a `Greeting`
|
/// Executes an EPP Hello call and returns the response as a `Greeting`
|
||||||
pub async fn hello(&mut self) -> Result<Greeting, Error> {
|
pub async fn hello(&mut self) -> Result<Greeting, Error> {
|
||||||
let xml = xml::serialize(&HelloDocument::default())?;
|
let xml = xml::serialize(&HelloDocument::default())?;
|
||||||
|
|
||||||
debug!(registry = self.connection.registry, "hello: {}", &xml);
|
debug!(registry = %self.inner.registry, "hello: {}", &xml);
|
||||||
let response = self.connection.transact(&xml).await?;
|
let response = self.inner.send(xml)?.await?;
|
||||||
debug!(
|
debug!(
|
||||||
registry = self.connection.registry,
|
registry = %self.inner.registry,
|
||||||
"greeting: {}", &response
|
"greeting: {}", &response
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -94,14 +104,16 @@ impl<C: Connector> EppClient<C> {
|
||||||
let document = CommandDocument::new(data.command, data.extension, id);
|
let document = CommandDocument::new(data.command, data.extension, id);
|
||||||
let xml = xml::serialize(&document)?;
|
let xml = xml::serialize(&document)?;
|
||||||
|
|
||||||
debug!(registry = self.connection.registry, "request: {}", &xml);
|
debug!(registry = %self.inner.registry, "request: {}", &xml);
|
||||||
let response = self.connection.transact(&xml).await?;
|
let response = self.inner.send(xml)?.await?;
|
||||||
debug!(
|
debug!(
|
||||||
registry = self.connection.registry,
|
registry = %self.inner.registry,
|
||||||
"response: {}", &response
|
"response: {}", &response
|
||||||
);
|
);
|
||||||
|
|
||||||
let rsp = xml::deserialize::<ResponseDocument<Cmd::Response, Ext::Response>>(&response)?;
|
let rsp = xml::deserialize::<ResponseDocument<Cmd::Response, Ext::Response>>(&response)?;
|
||||||
|
debug_assert!(rsp.data.tr_ids.client_tr_id.as_deref() == Some(id));
|
||||||
|
|
||||||
if rsp.data.result.code.is_success() {
|
if rsp.data.result.code.is_success() {
|
||||||
return Ok(rsp.data);
|
return Ok(rsp.data);
|
||||||
}
|
}
|
||||||
|
@ -111,32 +123,35 @@ impl<C: Connector> EppClient<C> {
|
||||||
tr_ids: rsp.data.tr_ids,
|
tr_ids: rsp.data.tr_ids,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
error!(registry=self.connection.registry, %response, "Failed to deserialize response for transaction: {}", err);
|
error!(
|
||||||
|
registry = %self.inner.registry,
|
||||||
|
%response,
|
||||||
|
"Failed to deserialize response for transaction: {}", err
|
||||||
|
);
|
||||||
Err(err)
|
Err(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Accepts raw EPP XML and returns the raw EPP XML response to it.
|
/// 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
|
/// Not recommended for direct use but sometimes can be useful for debugging
|
||||||
pub async fn transact_xml(&mut self, xml: &str) -> Result<String, Error> {
|
pub async fn transact_xml(&mut self, xml: String) -> Result<String, Error> {
|
||||||
self.connection.transact(xml).await
|
self.inner.send(xml)?.await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the greeting received on establishment of the connection in raw xml form
|
/// Returns the greeting received on establishment of the connection in raw xml form
|
||||||
pub fn xml_greeting(&self) -> String {
|
pub async fn xml_greeting(&self) -> Result<String, Error> {
|
||||||
String::from(&self.connection.greeting)
|
self.inner.xml_greeting().await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the greeting received on establishment of the connection as an `Greeting`
|
/// Returns the greeting received on establishment of the connection as an `Greeting`
|
||||||
pub fn greeting(&self) -> Result<Greeting, Error> {
|
pub async fn greeting(&self) -> Result<Greeting, Error> {
|
||||||
xml::deserialize::<GreetingDocument>(&self.connection.greeting).map(|obj| obj.data)
|
let greeting = self.inner.xml_greeting().await?;
|
||||||
|
xml::deserialize::<GreetingDocument>(&greeting).map(|obj| obj.data)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn reconnect(&mut self) -> Result<(), Error> {
|
/// Reconnects the underlying [`Connector::Connection`]
|
||||||
self.connection.reconnect().await
|
pub async fn reconnect(&self) -> Result<Greeting, Error> {
|
||||||
}
|
let greeting = self.inner.reconnect().await?;
|
||||||
|
xml::deserialize::<GreetingDocument>(&greeting).map(|obj| obj.data)
|
||||||
pub async fn shutdown(mut self) -> Result<(), Error> {
|
|
||||||
self.connection.shutdown().await
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -176,3 +191,63 @@ impl<'c, 'e, C, E> Clone for RequestData<'c, 'e, C, E> {
|
||||||
|
|
||||||
// Manual impl because this does not depend on whether `C` and `E` are `Copy`
|
// 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> {}
|
impl<'c, 'e, C, E> Copy for RequestData<'c, 'e, C, E> {}
|
||||||
|
|
||||||
|
struct InnerClient {
|
||||||
|
sender: mpsc::UnboundedSender<Request>,
|
||||||
|
pub registry: Cow<'static, str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InnerClient {
|
||||||
|
fn send(&self, request: String) -> Result<InnerResponse, Error> {
|
||||||
|
let (sender, receiver) = mpsc::channel(1);
|
||||||
|
let request = Request {
|
||||||
|
request: RequestMessage::Request(request),
|
||||||
|
sender,
|
||||||
|
};
|
||||||
|
self.sender.send(request).map_err(|_| Error::Closed)?;
|
||||||
|
|
||||||
|
Ok(InnerResponse { receiver })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the greeting received on establishment of the connection in raw xml form
|
||||||
|
async fn xml_greeting(&self) -> Result<String, Error> {
|
||||||
|
let (sender, receiver) = mpsc::channel(1);
|
||||||
|
let request = Request {
|
||||||
|
request: RequestMessage::Greeting,
|
||||||
|
sender,
|
||||||
|
};
|
||||||
|
self.sender.send(request).map_err(|_| Error::Closed)?;
|
||||||
|
|
||||||
|
InnerResponse { receiver }.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn reconnect(&self) -> Result<String, Error> {
|
||||||
|
let (sender, receiver) = mpsc::channel(1);
|
||||||
|
let request = Request {
|
||||||
|
request: RequestMessage::Reconnect,
|
||||||
|
sender,
|
||||||
|
};
|
||||||
|
self.sender.send(request).map_err(|_| Error::Closed)?;
|
||||||
|
|
||||||
|
InnerResponse { receiver }.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We do not need to parse any output at this point (we could do that),
|
||||||
|
// but for now we just store the receiver here.
|
||||||
|
pub(crate) struct InnerResponse {
|
||||||
|
receiver: mpsc::Receiver<Result<String, Error>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Future for InnerResponse {
|
||||||
|
type Output = Result<String, Error>;
|
||||||
|
|
||||||
|
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||||
|
let this = self.get_mut();
|
||||||
|
match this.receiver.poll_recv(cx) {
|
||||||
|
Poll::Ready(Some(response)) => Poll::Ready(response),
|
||||||
|
Poll::Ready(None) => Poll::Ready(Err(Error::Closed)),
|
||||||
|
Poll::Pending => Poll::Pending,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
use std::borrow::Cow;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
@ -8,6 +9,7 @@ use tokio::io::AsyncWrite;
|
||||||
#[cfg(feature = "tokio-rustls")]
|
#[cfg(feature = "tokio-rustls")]
|
||||||
use tokio::net::lookup_host;
|
use tokio::net::lookup_host;
|
||||||
use tokio::net::TcpStream;
|
use tokio::net::TcpStream;
|
||||||
|
use tokio::sync::mpsc;
|
||||||
#[cfg(feature = "tokio-rustls")]
|
#[cfg(feature = "tokio-rustls")]
|
||||||
use tokio_rustls::client::TlsStream;
|
use tokio_rustls::client::TlsStream;
|
||||||
#[cfg(feature = "tokio-rustls")]
|
#[cfg(feature = "tokio-rustls")]
|
||||||
|
@ -19,6 +21,7 @@ use tracing::info;
|
||||||
use crate::client::EppClient;
|
use crate::client::EppClient;
|
||||||
use crate::common::{Certificate, PrivateKey};
|
use crate::common::{Certificate, PrivateKey};
|
||||||
use crate::connection;
|
use crate::connection;
|
||||||
|
use crate::connection::EppConnection;
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
|
|
||||||
/// Connect to the specified `server` and `hostname` over TLS
|
/// Connect to the specified `server` and `hostname` over TLS
|
||||||
|
@ -33,15 +36,18 @@ use crate::error::Error;
|
||||||
/// Use connect_with_connector for passing a specific connector.
|
/// Use connect_with_connector for passing a specific connector.
|
||||||
#[cfg(feature = "tokio-rustls")]
|
#[cfg(feature = "tokio-rustls")]
|
||||||
pub async fn connect(
|
pub async fn connect(
|
||||||
registry: String,
|
registry: Cow<'static, str>,
|
||||||
server: (String, u16),
|
server: (Cow<'static, str>, u16),
|
||||||
identity: Option<(Vec<Certificate>, PrivateKey)>,
|
identity: Option<(Vec<Certificate>, PrivateKey)>,
|
||||||
timeout: Duration,
|
request_timeout: Duration,
|
||||||
) -> Result<EppClient<RustlsConnector>, Error> {
|
) -> Result<(EppClient, EppConnection<RustlsConnector>), Error> {
|
||||||
info!("Connecting to server: {:?}", server);
|
|
||||||
|
|
||||||
let connector = RustlsConnector::new(server, identity).await?;
|
let connector = RustlsConnector::new(server, identity).await?;
|
||||||
EppClient::new(connector, registry, timeout).await
|
|
||||||
|
let (sender, receiver) = mpsc::unbounded_channel();
|
||||||
|
let client = EppClient::new(sender, registry.clone());
|
||||||
|
let connection = EppConnection::new(connector, registry, receiver, request_timeout).await?;
|
||||||
|
|
||||||
|
Ok((client, connection))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Connect to the specified `server` and `hostname` via the passed connector.
|
/// Connect to the specified `server` and `hostname` via the passed connector.
|
||||||
|
@ -56,25 +62,29 @@ pub async fn connect(
|
||||||
/// Use connect_with_connector for passing a specific connector.
|
/// Use connect_with_connector for passing a specific connector.
|
||||||
pub async fn connect_with_connector<C>(
|
pub async fn connect_with_connector<C>(
|
||||||
connector: C,
|
connector: C,
|
||||||
registry: String,
|
registry: Cow<'static, str>,
|
||||||
timeout: Duration,
|
request_timeout: Duration,
|
||||||
) -> Result<EppClient<C>, Error>
|
) -> Result<(EppClient, EppConnection<C>), Error>
|
||||||
where
|
where
|
||||||
C: Connector,
|
C: Connector,
|
||||||
{
|
{
|
||||||
EppClient::new(connector, registry, timeout).await
|
let (sender, receiver) = mpsc::unbounded_channel();
|
||||||
|
let client = EppClient::new(sender, registry.clone());
|
||||||
|
let connection = EppConnection::new(connector, registry, receiver, request_timeout).await?;
|
||||||
|
|
||||||
|
Ok((client, connection))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "tokio-rustls")]
|
#[cfg(feature = "tokio-rustls")]
|
||||||
pub struct RustlsConnector {
|
pub struct RustlsConnector {
|
||||||
inner: TlsConnector,
|
inner: TlsConnector,
|
||||||
domain: ServerName,
|
domain: ServerName,
|
||||||
server: (String, u16),
|
server: (Cow<'static, str>, u16),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RustlsConnector {
|
impl RustlsConnector {
|
||||||
pub async fn new(
|
pub async fn new(
|
||||||
server: (String, u16),
|
server: (Cow<'static, str>, u16),
|
||||||
identity: Option<(Vec<Certificate>, PrivateKey)>,
|
identity: Option<(Vec<Certificate>, PrivateKey)>,
|
||||||
) -> Result<Self, Error> {
|
) -> Result<Self, Error> {
|
||||||
let mut roots = RootCertStore::empty();
|
let mut roots = RootCertStore::empty();
|
||||||
|
@ -103,7 +113,7 @@ impl RustlsConnector {
|
||||||
None => builder.with_no_client_auth(),
|
None => builder.with_no_client_auth(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let domain = server.0.as_str().try_into().map_err(|_| {
|
let domain = server.0.as_ref().try_into().map_err(|_| {
|
||||||
io::Error::new(
|
io::Error::new(
|
||||||
io::ErrorKind::InvalidInput,
|
io::ErrorKind::InvalidInput,
|
||||||
format!("Invalid domain: {}", server.0),
|
format!("Invalid domain: {}", server.0),
|
||||||
|
@ -125,7 +135,10 @@ impl Connector for RustlsConnector {
|
||||||
|
|
||||||
async fn connect(&self, timeout: Duration) -> Result<Self::Connection, Error> {
|
async fn connect(&self, timeout: Duration) -> Result<Self::Connection, Error> {
|
||||||
info!("Connecting to server: {}:{}", self.server.0, self.server.1);
|
info!("Connecting to server: {}:{}", self.server.0, self.server.1);
|
||||||
let addr = match lookup_host(&self.server).await?.next() {
|
let addr = match lookup_host((self.server.0.as_ref(), self.server.1))
|
||||||
|
.await?
|
||||||
|
.next()
|
||||||
|
{
|
||||||
Some(addr) => addr,
|
Some(addr) => addr,
|
||||||
None => {
|
None => {
|
||||||
return Err(Error::Io(io::Error::new(
|
return Err(Error::Io(io::Error::new(
|
||||||
|
|
|
@ -1,37 +1,51 @@
|
||||||
//! Manages registry connections and reading/writing to them
|
//! Manages registry connections and reading/writing to them
|
||||||
|
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::convert::TryInto;
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use std::{io, str, u32};
|
use std::{io, str, u32};
|
||||||
|
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
use tracing::{debug, info};
|
use tokio::sync::mpsc;
|
||||||
|
use tracing::{debug, error, info, trace, warn};
|
||||||
|
|
||||||
use crate::connect::Connector;
|
use crate::connect::Connector;
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
|
|
||||||
/// EPP Connection struct with some metadata for the connection
|
/// EPP Connection
|
||||||
pub(crate) struct EppConnection<C: Connector> {
|
///
|
||||||
pub registry: String,
|
/// This is the I/O half, returned when creating a new connection, that performs the actual I/O and thus
|
||||||
|
/// should be spawned in it's own task.
|
||||||
|
///
|
||||||
|
/// [`EppConnection`] provides a [`EppConnection::run`](EppConnection::run) method, which only resolves when the connection is closed,
|
||||||
|
/// either because a fatal error has occurred, or because its associated [`EppClient`](super::EppClient) has been dropped
|
||||||
|
/// and all outstanding work has been completed.
|
||||||
|
pub struct EppConnection<C: Connector> {
|
||||||
|
registry: Cow<'static, str>,
|
||||||
connector: C,
|
connector: C,
|
||||||
stream: C::Connection,
|
stream: C::Connection,
|
||||||
pub greeting: String,
|
greeting: String,
|
||||||
timeout: Duration,
|
timeout: Duration,
|
||||||
|
/// A receiver for receiving requests from [`EppClients`](super::client::EppClient) for the underlying connection.
|
||||||
|
receiver: mpsc::UnboundedReceiver<Request>,
|
||||||
state: ConnectionState,
|
state: ConnectionState,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<C: Connector> EppConnection<C> {
|
impl<C: Connector> EppConnection<C> {
|
||||||
pub(crate) async fn new(
|
pub(crate) async fn new(
|
||||||
connector: C,
|
connector: C,
|
||||||
registry: String,
|
registry: Cow<'static, str>,
|
||||||
timeout: Duration,
|
receiver: mpsc::UnboundedReceiver<Request>,
|
||||||
|
request_timeout: Duration,
|
||||||
) -> Result<Self, Error> {
|
) -> Result<Self, Error> {
|
||||||
let mut this = Self {
|
let mut this = Self {
|
||||||
registry,
|
registry,
|
||||||
stream: connector.connect(timeout).await?,
|
stream: connector.connect(request_timeout).await?,
|
||||||
connector,
|
connector,
|
||||||
|
receiver,
|
||||||
greeting: String::new(),
|
greeting: String::new(),
|
||||||
timeout,
|
timeout: request_timeout,
|
||||||
state: Default::default(),
|
state: Default::default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -40,7 +54,39 @@ impl<C: Connector> EppConnection<C> {
|
||||||
Ok(this)
|
Ok(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Constructs an EPP XML request in the required form and sends it to the server
|
/// Runs the connection
|
||||||
|
///
|
||||||
|
/// This will loops and awaits new requests from the client half and sends the request to the epp server
|
||||||
|
/// awaiting a response.
|
||||||
|
///
|
||||||
|
/// Spawn this in a task and await run to resolve.
|
||||||
|
/// This resolves when the connection to the epp server gets dropped.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```[no_compile]
|
||||||
|
/// let mut connection = <obtained via connect::connect()>
|
||||||
|
/// tokio::spawn(async move {
|
||||||
|
/// if let Err(err) = connection.run().await {
|
||||||
|
/// error!("connection failed: {err}")
|
||||||
|
/// }
|
||||||
|
/// });
|
||||||
|
pub async fn run(&mut self) -> Result<(), Error> {
|
||||||
|
while let Some(message) = self.message().await {
|
||||||
|
match message {
|
||||||
|
Ok(message) => info!("{message}"),
|
||||||
|
Err(err) => {
|
||||||
|
error!("{err}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
trace!("stopping EppConnection task");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sends the given content to the used [`Connector::Connection`]
|
||||||
|
///
|
||||||
|
/// Returns an EOF error when writing to the stream results in 0 bytes written.
|
||||||
async fn write_epp_request(&mut self, content: &str) -> Result<(), Error> {
|
async fn write_epp_request(&mut self, content: &str) -> Result<(), Error> {
|
||||||
let len = content.len();
|
let len = content.len();
|
||||||
|
|
||||||
|
@ -54,12 +100,26 @@ impl<C: Connector> EppConnection<C> {
|
||||||
buf[4..].clone_from_slice(content.as_bytes());
|
buf[4..].clone_from_slice(content.as_bytes());
|
||||||
|
|
||||||
let wrote = timeout(self.timeout, self.stream.write(&buf)).await?;
|
let wrote = timeout(self.timeout, self.stream.write(&buf)).await?;
|
||||||
debug!(registry = self.registry, "Wrote {} bytes", wrote);
|
// A write return value of 0 means the underlying socket
|
||||||
|
// does no longer accept any data.
|
||||||
|
if wrote == 0 {
|
||||||
|
warn!("Got EOF while writing");
|
||||||
|
self.state = ConnectionState::Closed;
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::UnexpectedEof,
|
||||||
|
format!("{}: unexpected eof", self.registry),
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
debug!(registry = %self.registry, "Wrote {} bytes", wrote);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Receives response from the socket and converts it into an EPP XML string
|
/// Receives response from the socket and converts it into an EPP XML string
|
||||||
async fn read_epp_response(&mut self) -> Result<String, Error> {
|
async fn read_epp_response(&mut self) -> Result<String, Error> {
|
||||||
|
// We're looking for the frame header which tells us how long the response will be.
|
||||||
|
// The frame header is a 32-bit (4-byte) big-endian unsigned integer.
|
||||||
let mut buf = [0u8; 4];
|
let mut buf = [0u8; 4];
|
||||||
timeout(self.timeout, self.stream.read_exact(&mut buf)).await?;
|
timeout(self.timeout, self.stream.read_exact(&mut buf)).await?;
|
||||||
|
|
||||||
|
@ -67,7 +127,7 @@ impl<C: Connector> EppConnection<C> {
|
||||||
|
|
||||||
let message_size = buf_size - 4;
|
let message_size = buf_size - 4;
|
||||||
debug!(
|
debug!(
|
||||||
registry = self.registry,
|
registry = %self.registry,
|
||||||
"Response buffer size: {}", message_size
|
"Response buffer size: {}", message_size
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -76,10 +136,10 @@ impl<C: Connector> EppConnection<C> {
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let read = timeout(self.timeout, self.stream.read(&mut buf[read_size..])).await?;
|
let read = timeout(self.timeout, self.stream.read(&mut buf[read_size..])).await?;
|
||||||
debug!(registry = self.registry, "Read: {} bytes", read);
|
debug!(registry = %self.registry, "Read: {} bytes", read);
|
||||||
|
|
||||||
read_size += read;
|
read_size += read;
|
||||||
debug!(registry = self.registry, "Total read: {} bytes", read_size);
|
debug!(registry = %self.registry, "Total read: {} bytes", read_size);
|
||||||
|
|
||||||
if read == 0 {
|
if read == 0 {
|
||||||
self.state = ConnectionState::Closed;
|
self.state = ConnectionState::Closed;
|
||||||
|
@ -96,8 +156,8 @@ impl<C: Connector> EppConnection<C> {
|
||||||
Ok(String::from_utf8(buf)?)
|
Ok(String::from_utf8(buf)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn reconnect(&mut self) -> Result<(), Error> {
|
async fn reconnect(&mut self) -> Result<(), Error> {
|
||||||
debug!(registry = self.registry, "reconnecting");
|
debug!(registry = %self.registry, "reconnecting");
|
||||||
self.state = ConnectionState::Opening;
|
self.state = ConnectionState::Opening;
|
||||||
self.stream = self.connector.connect(self.timeout).await?;
|
self.stream = self.connector.connect(self.timeout).await?;
|
||||||
self.greeting = self.read_epp_response().await?;
|
self.greeting = self.read_epp_response().await?;
|
||||||
|
@ -105,30 +165,68 @@ impl<C: Connector> EppConnection<C> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sends an EPP XML request to the registry and return the response
|
async fn wait_for_shutdown(&mut self) -> Result<(), io::Error> {
|
||||||
/// receieved to the request
|
self.state = ConnectionState::Closing;
|
||||||
pub(crate) async fn transact(&mut self, content: &str) -> Result<String, Error> {
|
match self.stream.shutdown().await {
|
||||||
if self.state != ConnectionState::Open {
|
Ok(_) => {
|
||||||
debug!(registry = self.registry, " connection not ready");
|
self.state = ConnectionState::Closed;
|
||||||
self.reconnect().await?;
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(err) => Err(err),
|
||||||
}
|
}
|
||||||
|
|
||||||
debug!(registry = self.registry, " request: {}", content);
|
|
||||||
self.write_epp_request(content).await?;
|
|
||||||
|
|
||||||
let response = self.read_epp_response().await?;
|
|
||||||
debug!(registry = self.registry, " response: {}", response);
|
|
||||||
|
|
||||||
Ok(response)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Closes the socket and shuts the connection
|
/// This is the main method of the I/O tasks
|
||||||
pub(crate) async fn shutdown(&mut self) -> Result<(), Error> {
|
///
|
||||||
info!(registry = self.registry, "Closing connection");
|
/// It will try to get a request, write it to the wire and waits for the response.
|
||||||
self.state = ConnectionState::Closing;
|
///
|
||||||
timeout(self.timeout, self.stream.shutdown()).await?;
|
/// Once this returns `None`, or `Ok(Err(_))`, the connection is expected to be closed.
|
||||||
self.state = ConnectionState::Closed;
|
async fn message(&mut self) -> Option<Result<Cow<'static, str>, Error>> {
|
||||||
Ok(())
|
// In theory this can be even speed up as the underlying stream is in our case bi-directional.
|
||||||
|
// But as the EPP RFC does not guarantee the order of responses we would need to
|
||||||
|
// match based on the transactions id. We can look into adding support for this in
|
||||||
|
// future.
|
||||||
|
loop {
|
||||||
|
if self.state == ConnectionState::Closed {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for new request
|
||||||
|
let request = self.receiver.recv().await;
|
||||||
|
let Some(request) = request else {
|
||||||
|
// The client got dropped. We can close the connection.
|
||||||
|
match self.wait_for_shutdown().await {
|
||||||
|
Ok(_) => return None,
|
||||||
|
Err(err) => return Some(Err(err.into())),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = match request.request {
|
||||||
|
RequestMessage::Greeting => Ok(self.greeting.clone()),
|
||||||
|
RequestMessage::Request(request) => {
|
||||||
|
if let Err(err) = self.write_epp_request(&request).await {
|
||||||
|
return Some(Err(err));
|
||||||
|
}
|
||||||
|
timeout(self.timeout, self.read_epp_response()).await
|
||||||
|
}
|
||||||
|
RequestMessage::Reconnect => match self.reconnect().await {
|
||||||
|
Ok(_) => Ok(self.greeting.clone()),
|
||||||
|
Err(err) => {
|
||||||
|
// In this case we are not sure if the connection is in tact. Best we error out.
|
||||||
|
let _ = request.sender.send(Err(Error::Reconnect)).await;
|
||||||
|
return Some(Err(err));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Awaiting `send` should not block this I/O tasks unless we try to write multiple responses to the same bounded channel.
|
||||||
|
// As this crate is structured to create a new bounded channel for each request, this ok here.
|
||||||
|
if request.sender.send(response).await.is_err() {
|
||||||
|
// If the receive half of the sender is dropped, (i.e. the `Client`s `Future` is canceled)
|
||||||
|
// we can just ignore the err here and return to let `run` print something for this task.
|
||||||
|
return Some(Ok("request was canceled. Client dropped.".into()));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -151,3 +249,17 @@ enum ConnectionState {
|
||||||
Closing,
|
Closing,
|
||||||
Closed,
|
Closed,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) struct Request {
|
||||||
|
pub(crate) request: RequestMessage,
|
||||||
|
pub(crate) sender: mpsc::Sender<Result<String, Error>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) enum RequestMessage {
|
||||||
|
/// Request the stored server greeting
|
||||||
|
Greeting,
|
||||||
|
/// Reconnect the underlying [`Connector::Connection`]
|
||||||
|
Reconnect,
|
||||||
|
/// Raw request to be sent to the connected EPP Server
|
||||||
|
Request(String),
|
||||||
|
}
|
||||||
|
|
|
@ -12,8 +12,10 @@ use crate::response::ResponseStatus;
|
||||||
/// Error enum holding the possible error types
|
/// Error enum holding the possible error types
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
|
Closed,
|
||||||
Command(Box<ResponseStatus>),
|
Command(Box<ResponseStatus>),
|
||||||
Io(std::io::Error),
|
Io(std::io::Error),
|
||||||
|
Reconnect,
|
||||||
Timeout,
|
Timeout,
|
||||||
Xml(Box<dyn StdError + Send + Sync>),
|
Xml(Box<dyn StdError + Send + Sync>),
|
||||||
Other(Box<dyn StdError + Send + Sync>),
|
Other(Box<dyn StdError + Send + Sync>),
|
||||||
|
@ -24,10 +26,12 @@ impl StdError for Error {}
|
||||||
impl Display for Error {
|
impl Display for Error {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
|
Error::Closed => write!(f, "closed"),
|
||||||
Error::Command(e) => {
|
Error::Command(e) => {
|
||||||
write!(f, "command error: {}", e.result.message)
|
write!(f, "command error: {}", e.result.message)
|
||||||
}
|
}
|
||||||
Error::Io(e) => write!(f, "I/O error: {}", e),
|
Error::Io(e) => write!(f, "I/O error: {}", e),
|
||||||
|
Error::Reconnect => write!(f, "failed to reconnect"),
|
||||||
Error::Timeout => write!(f, "timeout"),
|
Error::Timeout => write!(f, "timeout"),
|
||||||
Error::Xml(e) => write!(f, "(de)serialization error: {}", e),
|
Error::Xml(e) => write!(f, "(de)serialization error: {}", e),
|
||||||
Error::Other(e) => write!(f, "error: {}", e),
|
Error::Other(e) => write!(f, "error: {}", e),
|
||||||
|
|
10
src/lib.rs
10
src/lib.rs
|
@ -59,7 +59,7 @@
|
||||||
//! use std::net::ToSocketAddrs;
|
//! use std::net::ToSocketAddrs;
|
||||||
//! use std::time::Duration;
|
//! use std::time::Duration;
|
||||||
//!
|
//!
|
||||||
//! use epp_client::EppClient;
|
//! use epp_client::connect::connect;
|
||||||
//! use epp_client::domain::DomainCheck;
|
//! use epp_client::domain::DomainCheck;
|
||||||
//! use epp_client::login::Login;
|
//! use epp_client::login::Login;
|
||||||
//!
|
//!
|
||||||
|
@ -67,11 +67,15 @@
|
||||||
//! async fn main() {
|
//! async fn main() {
|
||||||
//! // Create an instance of EppClient
|
//! // Create an instance of EppClient
|
||||||
//! let timeout = Duration::from_secs(5);
|
//! let timeout = Duration::from_secs(5);
|
||||||
//! let mut client = match EppClient::connect("registry_name".to_string(), ("example.com".to_owned(), 7000), None, timeout).await {
|
//! let (mut client, mut connection) = match connect("registry_name".into(), ("example.com".into(), 7000), None, timeout).await {
|
||||||
//! Ok(client) => client,
|
//! Ok(c) => c,
|
||||||
//! Err(e) => panic!("Failed to create EppClient: {}", e)
|
//! Err(e) => panic!("Failed to create EppClient: {}", e)
|
||||||
//! };
|
//! };
|
||||||
//!
|
//!
|
||||||
|
//! tokio::spawn(async move {
|
||||||
|
//! connection.run().await.unwrap();
|
||||||
|
//! });
|
||||||
|
//!
|
||||||
//! let login = Login::new("username", "password", None, None);
|
//! let login = Login::new("username", "password", None, None);
|
||||||
//! client.transact(&login, "transaction-id").await.unwrap();
|
//! client.transact(&login, "transaction-id").await.unwrap();
|
||||||
//!
|
//!
|
||||||
|
|
|
@ -8,6 +8,7 @@ use epp_client::connect::connect_with_connector;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use tokio::time::timeout;
|
use tokio::time::timeout;
|
||||||
use tokio_test::io::Builder;
|
use tokio_test::io::Builder;
|
||||||
|
use tracing::trace;
|
||||||
|
|
||||||
use epp_client::domain::{DomainCheck, DomainContact, DomainCreate, Period};
|
use epp_client::domain::{DomainCheck, DomainContact, DomainCreate, Period};
|
||||||
use epp_client::login::Login;
|
use epp_client::login::Login;
|
||||||
|
@ -99,11 +100,19 @@ async fn client() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut client = connect_with_connector(FakeConnector, "test".into(), Duration::from_secs(5))
|
let (mut client, mut connection) =
|
||||||
.await
|
connect_with_connector(FakeConnector, "test".into(), Duration::from_secs(5))
|
||||||
.unwrap();
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(client.xml_greeting(), xml("response/greeting.xml"));
|
tokio::spawn(async move {
|
||||||
|
connection.run().await.unwrap();
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
client.xml_greeting().await.unwrap(),
|
||||||
|
xml("response/greeting.xml")
|
||||||
|
);
|
||||||
let rsp = client
|
let rsp = client
|
||||||
.transact(
|
.transact(
|
||||||
&Login::new(
|
&Login::new(
|
||||||
|
@ -116,6 +125,7 @@ async fn client() {
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
assert_eq!(rsp.result.code, ResultCode::CommandCompletedSuccessfully);
|
||||||
|
|
||||||
assert_eq!(rsp.result.code, ResultCode::CommandCompletedSuccessfully);
|
assert_eq!(rsp.result.code, ResultCode::CommandCompletedSuccessfully);
|
||||||
|
|
||||||
|
@ -175,11 +185,20 @@ async fn dropped() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut client = connect_with_connector(FakeConnector, "test".into(), Duration::from_secs(5))
|
let (mut client, mut connection) =
|
||||||
.await
|
connect_with_connector(FakeConnector, "test".into(), Duration::from_secs(5))
|
||||||
.unwrap();
|
.await
|
||||||
|
.unwrap();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
connection.run().await.unwrap();
|
||||||
|
trace!("connection future resolved successfully")
|
||||||
|
});
|
||||||
|
|
||||||
assert_eq!(client.xml_greeting(), xml("response/greeting.xml"));
|
trace!("Trying to get greeting");
|
||||||
|
assert_eq!(
|
||||||
|
client.xml_greeting().await.unwrap(),
|
||||||
|
xml("response/greeting.xml")
|
||||||
|
);
|
||||||
let rsp = client
|
let rsp = client
|
||||||
.transact(
|
.transact(
|
||||||
&Login::new(
|
&Login::new(
|
||||||
|
@ -240,4 +259,40 @@ async fn dropped() {
|
||||||
|
|
||||||
let rsp = client.transact(&create, CLTRID).await.unwrap();
|
let rsp = client.transact(&create, CLTRID).await.unwrap();
|
||||||
assert_eq!(rsp.result.code, ResultCode::CommandCompletedSuccessfully);
|
assert_eq!(rsp.result.code, ResultCode::CommandCompletedSuccessfully);
|
||||||
|
|
||||||
|
drop(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn drop_client() {
|
||||||
|
let _guard = log_to_stdout();
|
||||||
|
|
||||||
|
struct FakeConnector;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl epp_client::client::Connector for FakeConnector {
|
||||||
|
type Connection = tokio_test::io::Mock;
|
||||||
|
|
||||||
|
async fn connect(&self, _: Duration) -> Result<Self::Connection, epp_client::Error> {
|
||||||
|
Ok(build_stream(&["response/greeting.xml"]).build())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let (client, mut connection) =
|
||||||
|
connect_with_connector(FakeConnector, "test".into(), Duration::from_secs(5))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let handle = tokio::spawn(async move {
|
||||||
|
connection.run().await.unwrap();
|
||||||
|
trace!("connection future resolved successfully")
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
client.xml_greeting().await.unwrap(),
|
||||||
|
xml("response/greeting.xml")
|
||||||
|
);
|
||||||
|
|
||||||
|
drop(client);
|
||||||
|
assert!(handle.await.is_ok());
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue