introduce connection level extensions

Some EPP extensions such as change poll didn't map to the present command oriented extensions.

The <changePoll> element will be present in the poll response without any elements in the <extension> element of  the request.
There is no allowed value for the <extension> element for the poll command (if you only support RGP, NameStore, and ChangePoll).

This now stores enabled general extensions in the EppClient. This allows you to retrieve command level extensions responses using Response::command_extension
and connection level extensions using Response::connection_extension
This commit is contained in:
Rudi Floren 2024-12-16 20:07:11 +01:00
parent d50f10a2b0
commit 785d3f6771
7 changed files with 262 additions and 91 deletions

View File

@ -1,3 +1,4 @@
use std::marker::PhantomData;
use std::time::Duration;
#[cfg(feature = "__rustls")]
@ -10,7 +11,7 @@ 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::response::{ConnectionExtensionResponse, Response, ResponseStatus};
use crate::xml;
/// An `EppClient` provides an interface to sending EPP requests to a registry
@ -64,12 +65,13 @@ use crate::xml;
/// Domain: eppdev.com, Available: 1
/// Domain: eppdev.net, Available: 1
/// ```
pub struct EppClient<C: Connector> {
pub struct EppClient<C: Connector, ConnExt> {
connection: EppConnection<C>,
phantom_type: PhantomData<ConnExt>,
}
#[cfg(feature = "__rustls")]
impl EppClient<RustlsConnector> {
impl<ConnExt: ConnectionExtensionResponse> EppClient<RustlsConnector, ConnExt> {
/// Connect to the specified `addr` and `hostname` over TLS
///
/// The `registry` is used as a name in internal logging; `host` provides the host name
@ -97,11 +99,12 @@ impl EppClient<RustlsConnector> {
}
}
impl<C: Connector> EppClient<C> {
impl<C: Connector, ConnExt: ConnectionExtensionResponse> EppClient<C, ConnExt> {
/// Create an `EppClient` from an already established connection
pub async fn new(connector: C, registry: String, timeout: Duration) -> Result<Self, Error> {
Ok(Self {
connection: EppConnection::new(connector, registry, timeout).await?,
phantom_type: PhantomData,
})
}
@ -120,7 +123,7 @@ impl<C: Connector> EppClient<C> {
&mut self,
data: impl Into<RequestData<'c, 'e, Cmd, CmdExt>>,
id: &str,
) -> Result<Response<Cmd::Response, CmdExt::Response>, Error>
) -> Result<Response<Cmd::Response, CmdExt::Response, ConnExt>, Error>
where
Cmd: Transaction<CmdExt> + Command + 'c,
CmdExt: Extension + 'e,
@ -133,13 +136,15 @@ impl<C: Connector> EppClient<C> {
let response = self.connection.transact(&xml)?.await?;
debug!("{}: response: {}", self.connection.registry, &response);
let rsp = match xml::deserialize::<Response<Cmd::Response, CmdExt::Response>>(&response) {
Ok(rsp) => rsp,
Err(e) => {
error!(%response, "failed to deserialize response for transaction: {e}");
return Err(e);
}
};
let rsp =
match xml::deserialize::<Response<Cmd::Response, CmdExt::Response, ConnExt>>(&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);

View File

@ -7,18 +7,11 @@ use std::borrow::Cow;
use instant_xml::{Error, FromXml, ToXml};
use crate::{
poll::Poll,
request::{Extension, Transaction},
};
use crate::response::ConnectionExtensionResponse;
pub const XMLNS: &str = "urn:ietf:params:xml:ns:changePoll-1.0";
impl Transaction<ChangePoll<'_>> for Poll {}
impl Extension for ChangePoll<'_> {
type Response = ChangePoll<'static>;
}
impl ConnectionExtensionResponse for ChangePoll<'_> {}
/// Type for EPP XML `<changePoll>` extension
///
@ -204,13 +197,14 @@ pub enum State {
#[cfg(test)]
mod tests {
use super::*;
use crate::common::NoExtension;
use crate::poll::Poll;
use crate::response::ResultCode;
use crate::tests::{response_from_file_with_ext, CLTRID, SVTRID};
#[test]
fn urs_lock_before() {
let object = response_from_file_with_ext::<Poll, ChangePoll>(
let object = response_from_file_with_ext::<Poll, NoExtension, ChangePoll>(
"response/extensions/change_poll/urs_lock_before.xml",
);
@ -223,29 +217,18 @@ mod tests {
"Command completed successfully; ack to dequeue"
);
assert_eq!(object.extension().unwrap().state.unwrap(), State::Before);
let ext = &object.connection_extension().unwrap();
assert_eq!(ext.state.unwrap(), State::Before);
assert_eq!(ext.operation.kind().unwrap(), OperationKind::Update);
assert_eq!(ext.date, "2013-10-22T14:25:57.0Z");
assert_eq!(ext.server_tr_id, "12345-XYZ");
assert_eq!(ext.who, "URS Admin");
assert_eq!(
object.extension().unwrap().operation.kind().unwrap(),
OperationKind::Update
);
assert_eq!(object.extension().unwrap().date, "2013-10-22T14:25:57.0Z");
assert_eq!(object.extension().unwrap().server_tr_id, "12345-XYZ");
assert_eq!(object.extension().unwrap().who, "URS Admin");
assert_eq!(
object
.extension()
.unwrap()
.case_id
.as_ref()
.unwrap()
.kind()
.unwrap(),
ext.case_id.as_ref().unwrap().kind().unwrap(),
CaseIdentifierKind::Urs
);
assert_eq!(
object.extension().unwrap().reason.as_ref().unwrap().inner,
"URS Lock"
);
assert_eq!(ext.reason.as_ref().unwrap().inner, "URS Lock");
assert_eq!(object.tr_ids.client_tr_id.unwrap(), CLTRID);
assert_eq!(object.tr_ids.server_tr_id, SVTRID);
@ -253,7 +236,7 @@ mod tests {
#[test]
fn urs_lock_after() {
let object = response_from_file_with_ext::<Poll, ChangePoll>(
let object = response_from_file_with_ext::<Poll, NoExtension, ChangePoll>(
"response/extensions/change_poll/urs_lock_after.xml",
);
@ -265,7 +248,10 @@ mod tests {
object.result.message,
"Command completed successfully; ack to dequeue"
);
assert_eq!(object.extension().unwrap().state.unwrap(), State::After);
let ext = &object.connection_extension().unwrap();
assert_eq!(ext.state.unwrap(), State::After);
assert_eq!(object.tr_ids.client_tr_id.unwrap(), CLTRID);
assert_eq!(object.tr_ids.server_tr_id, SVTRID);
@ -273,7 +259,7 @@ mod tests {
#[test]
fn custom_sync_after() {
let object = response_from_file_with_ext::<Poll, ChangePoll>(
let object = response_from_file_with_ext::<Poll, NoExtension, ChangePoll>(
"response/extensions/change_poll/custom_sync_after.xml",
);
@ -286,15 +272,11 @@ mod tests {
"Command completed successfully; ack to dequeue"
);
assert_eq!(
object.extension().unwrap().operation.kind().unwrap(),
OperationKind::Custom("sync")
);
assert_eq!(object.extension().unwrap().who, "CSR");
assert_eq!(
object.extension().unwrap().reason.as_ref().unwrap().inner,
"Customer sync request"
);
let ext = &object.connection_extension().unwrap();
assert_eq!(ext.operation.kind().unwrap(), OperationKind::Custom("sync"));
assert_eq!(ext.who, "CSR");
assert_eq!(ext.reason.as_ref().unwrap().inner, "Customer sync request");
assert_eq!(object.tr_ids.client_tr_id.unwrap(), CLTRID);
assert_eq!(object.tr_ids.server_tr_id, SVTRID);
@ -302,7 +284,7 @@ mod tests {
#[test]
fn delete_before() {
let object = response_from_file_with_ext::<Poll, ChangePoll>(
let object = response_from_file_with_ext::<Poll, NoExtension, ChangePoll>(
"response/extensions/change_poll/delete_before.xml",
);
@ -321,7 +303,7 @@ mod tests {
#[test]
fn autopurge_before() {
let object = response_from_file_with_ext::<Poll, ChangePoll>(
let object = response_from_file_with_ext::<Poll, NoExtension, ChangePoll>(
"response/extensions/change_poll/autopurge_before.xml",
);
@ -340,7 +322,7 @@ mod tests {
#[test]
fn update_after() {
let object = response_from_file_with_ext::<Poll, ChangePoll>(
let object = response_from_file_with_ext::<Poll, NoExtension, ChangePoll>(
"response/extensions/change_poll/update_after.xml",
);

View File

@ -75,6 +75,7 @@ pub struct NameStore<'a> {
#[cfg(test)]
mod tests {
use super::NameStore;
use crate::common::NoExtension;
use crate::domain::check::DomainCheck;
use crate::tests::{assert_serialized, response_from_file_with_ext};
@ -94,10 +95,10 @@ mod tests {
#[test]
fn response() {
let object = response_from_file_with_ext::<DomainCheck, NameStore>(
let object = response_from_file_with_ext::<DomainCheck, NameStore, NoExtension>(
"response/extensions/namestore.xml",
);
let ext = object.extension().unwrap();
let ext = &object.command_extension().unwrap();
assert_eq!(ext.subproduct, "com");
}
}

View File

@ -69,6 +69,7 @@ pub enum RgpRequestResponse {
#[cfg(test)]
mod tests {
use super::{RgpRestoreRequest, Update};
use crate::common::NoExtension;
use crate::domain::info::DomainInfo;
use crate::domain::update::{DomainChangeInfo, DomainUpdate};
use crate::extensions::rgp::request::RgpRequestResponse;
@ -99,15 +100,16 @@ mod tests {
#[test]
fn request_response() {
let object = response_from_file_with_ext::<DomainUpdate, Update<RgpRestoreRequest>>(
"response/extensions/rgp_restore.xml",
);
let ext = object.extension.unwrap();
let object =
response_from_file_with_ext::<DomainUpdate, Update<RgpRestoreRequest>, NoExtension>(
"response/extensions/rgp_restore.xml",
);
let ext = object.command_extension().unwrap();
assert_eq!(object.result.code, ResultCode::CommandCompletedSuccessfully);
assert_eq!(object.result.message, SUCCESS_MSG);
let data = match ext.data {
let data = match ext {
RgpRequestResponse::Update(data) => data,
_ => panic!("Unexpected response type"),
};
@ -118,12 +120,13 @@ mod tests {
#[test]
fn domain_info_request_response() {
let object = response_from_file_with_ext::<DomainInfo, Update<RgpRestoreRequest>>(
"response/extensions/domain_info_rgp.xml",
);
let ext = object.extension.unwrap();
let object =
response_from_file_with_ext::<DomainInfo, Update<RgpRestoreRequest>, NoExtension>(
"response/extensions/domain_info_rgp.xml",
);
let ext = object.command_extension().unwrap();
let data = match ext.data {
let data = match ext {
RgpRequestResponse::Info(data) => data,
_ => panic!("Unexpected response type"),
};

View File

@ -3,9 +3,9 @@
use std::fmt::Debug;
use chrono::{DateTime, Utc};
use instant_xml::{FromXml, Kind};
use instant_xml::{Deserializer, FromXml, FromXmlOwned, Id, Kind};
use crate::common::EPP_XMLNS;
use crate::common::{NoExtension, EPP_XMLNS};
/// Type corresponding to the `<undef>` tag an EPP response XML
#[derive(Debug, Eq, FromXml, PartialEq)]
@ -238,7 +238,7 @@ pub struct Message {
/// Type corresponding to the `<response>` tag in an EPP response XML
/// containing an `<extension>` tag
#[xml(rename = "response", ns(EPP_XMLNS))]
pub struct Response<D, E> {
pub struct Response<D, CmdExt, ConnExt> {
/// Data under the `<result>` tag
pub result: EppResult,
/// Data under the `<msgQ>` tag
@ -247,7 +247,7 @@ pub struct Response<D, E> {
/// Data under the `<resData>` tag
pub res_data: Option<ResponseData<D>>,
/// Data under the `<extension>` tag
pub extension: Option<Extension<E>>,
pub extension: Option<Extension<CmdExt, ConnExt>>,
/// Data under the `<trID>` tag
pub tr_ids: ResponseTRID,
}
@ -276,7 +276,7 @@ pub struct ResponseStatus {
pub tr_ids: ResponseTRID,
}
impl<T, E> Response<T, E> {
impl<T, CmdExt, ConnExt> Response<T, CmdExt, ConnExt> {
/// Returns the data under the corresponding `<resData>` from the EPP XML
pub fn res_data(&self) -> Option<&T> {
match &self.res_data {
@ -285,9 +285,16 @@ impl<T, E> Response<T, E> {
}
}
pub fn extension(&self) -> Option<&E> {
pub fn command_extension(&self) -> Option<&CmdExt> {
match &self.extension {
Some(extension) => Some(&extension.data),
Some(extension) => extension.data.command.as_ref(),
None => None,
}
}
pub fn connection_extension(&self) -> Option<&ConnExt> {
match &self.extension {
Some(extension) => extension.data.connection.as_ref(),
None => None,
}
}
@ -303,8 +310,116 @@ impl<T, E> Response<T, E> {
#[derive(Debug, Eq, FromXml, PartialEq)]
#[xml(rename = "extension", ns(EPP_XMLNS))]
pub struct Extension<E> {
pub data: E,
pub struct Extension<CmdExt, ConnExt> {
pub data: CombinedExtensions<CmdExt, ConnExt>,
}
/// Types implementing this can be used as ConnectionExtensions.
///
/// Their type will be assumed to be in the response's extension element.
pub trait ConnectionExtensionResponse: FromXmlOwned + Debug {}
impl ConnectionExtensionResponse for NoExtension {}
/// Combines connection extensions and command extensions
///
/// Some extensions are sent by the server no matter if an extension was defined for the command.
#[derive(Debug, Eq, PartialEq)]
pub struct CombinedExtensions<CmdExt, ConnExt> {
pub command: Option<CmdExt>,
pub connection: Option<ConnExt>,
}
// // This manual impl is needed because instant-xml is not able to create the correct derived impl.
// // for this transparent type. It fails to set the correct bounds for struct __CombinedExtensionsAccumulator.
impl<'xml, CmdExt: FromXml<'xml>, ConnExt: FromXml<'xml>> FromXml<'xml>
for CombinedExtensions<CmdExt, ConnExt>
{
#[inline]
fn matches(id: Id<'_>, _: Option<Id<'_>>) -> bool {
<CmdExt as FromXml<'xml>>::matches(id, None)
|| <ConnExt as FromXml<'xml>>::matches(id, None)
}
fn deserialize<'cx>(
into: &mut Self::Accumulator,
_: &'static str,
deserializer: &mut Deserializer<'cx, 'xml>,
) -> Result<(), instant_xml::Error> {
use instant_xml::Kind;
let current = deserializer.parent();
if <CmdExt as FromXml<'xml>>::matches(current, None) {
match <CmdExt as FromXml>::KIND {
Kind::Element => {
<Option<CmdExt>>::deserialize(
&mut into.command,
"CombinedExtensions::command",
deserializer,
)?;
}
Kind::Scalar => {
<Option<CmdExt>>::deserialize(
&mut into.command,
"CombinedExtensions::command",
deserializer,
)?;
deserializer.ignore()?;
}
}
} else if <ConnExt as FromXml<'xml>>::matches(current, None) {
match <ConnExt as FromXml>::KIND {
Kind::Element => {
<Option<ConnExt>>::deserialize(
&mut into.connection,
"CombinedExtensions::connection",
deserializer,
)?;
}
Kind::Scalar => {
<Option<ConnExt>>::deserialize(
&mut into.connection,
"CombinedExtensions::connection",
deserializer,
)?;
deserializer.ignore()?;
}
}
}
Ok(())
}
type Accumulator = __CombinedExtensionsAccumulator<'xml, CmdExt, ConnExt>;
const KIND: instant_xml::Kind = instant_xml::Kind::Element;
}
pub struct __CombinedExtensionsAccumulator<'xml, CommandExt: FromXml<'xml>, ConnExt: FromXml<'xml>>
{
command: <Option<CommandExt> as FromXml<'xml>>::Accumulator,
connection: <Option<ConnExt> as FromXml<'xml>>::Accumulator,
}
impl<'xml, CommandExt: FromXml<'xml>, ConnExt: FromXml<'xml>>
instant_xml::Accumulate<CombinedExtensions<CommandExt, ConnExt>>
for __CombinedExtensionsAccumulator<'xml, CommandExt, ConnExt>
{
fn try_done(
self,
_: &'static str,
) -> Result<CombinedExtensions<CommandExt, ConnExt>, instant_xml::Error> {
Ok(CombinedExtensions {
command: self.command.try_done("CombinedExtensions::command")?,
connection: self.connection.try_done("CombinedExtensions::connection")?,
})
}
}
impl<'xml, CommandExt: FromXml<'xml>, ConnExt: instant_xml::FromXml<'xml>> Default
for __CombinedExtensionsAccumulator<'xml, CommandExt, ConnExt>
{
fn default() -> Self {
Self {
command: Default::default(),
connection: Default::default(),
}
}
}
#[cfg(test)]

View File

@ -9,7 +9,7 @@ use crate::{
client::RequestData,
common::NoExtension,
request::{Command, CommandWrapper, Extension, Transaction},
response::Response,
response::{ConnectionExtensionResponse, Response},
xml,
};
@ -53,23 +53,24 @@ pub(crate) fn assert_serialized<'c, 'e, Cmd, Ext>(
pub(crate) fn response_from_file<'c, Cmd>(
path: &str,
) -> Response<Cmd::Response, <NoExtension as Extension>::Response>
) -> Response<Cmd::Response, <NoExtension as Extension>::Response, NoExtension>
where
Cmd: Transaction<NoExtension> + Command + 'c,
{
response_from_file_with_ext::<Cmd, NoExtension>(path)
response_from_file_with_ext::<Cmd, NoExtension, NoExtension>(path)
}
pub(crate) fn response_from_file_with_ext<Cmd, Ext>(
pub(crate) fn response_from_file_with_ext<Cmd, CmdExt, ConnExt>(
path: &str,
) -> Response<Cmd::Response, Ext::Response>
) -> Response<Cmd::Response, CmdExt::Response, ConnExt>
where
Cmd: Transaction<NoExtension> + Command,
Ext: Extension,
CmdExt: Extension,
ConnExt: ConnectionExtensionResponse,
{
let xml = get_xml(path).unwrap();
dbg!(&xml);
let rsp = xml::deserialize::<Response<Cmd::Response, Ext::Response>>(&xml).unwrap();
let rsp = xml::deserialize::<Response<Cmd::Response, CmdExt::Response, ConnExt>>(&xml).unwrap();
assert!(rsp.result.code.is_success());
rsp
}

View File

@ -4,11 +4,14 @@ use std::str;
use std::time::Duration;
use async_trait::async_trait;
use instant_epp::extensions::change_poll::ChangePoll;
use instant_epp::poll::{Poll, PollData};
use regex::Regex;
use tokio::time::timeout;
use tokio_test::io::Builder;
use instant_epp::client::{Connector, EppClient};
use instant_epp::common::NoExtension;
use instant_epp::domain::{DomainCheck, DomainContact, DomainCreate, Period, PeriodLength};
use instant_epp::login::Login;
use instant_epp::response::ResultCode;
@ -100,9 +103,10 @@ async fn client() {
}
}
let mut client = EppClient::new(FakeConnector, "test".into(), Duration::from_secs(5))
.await
.unwrap();
let mut client =
EppClient::<_, NoExtension>::new(FakeConnector, "test".into(), Duration::from_secs(5))
.await
.unwrap();
assert_eq!(client.xml_greeting(), xml("response/greeting.xml"));
let rsp = client
@ -176,9 +180,10 @@ async fn dropped() {
}
}
let mut client = EppClient::new(FakeConnector, "test".into(), Duration::from_secs(5))
.await
.unwrap();
let mut client =
EppClient::<_, NoExtension>::new(FakeConnector, "test".into(), Duration::from_secs(5))
.await
.unwrap();
assert_eq!(client.xml_greeting(), xml("response/greeting.xml"));
let rsp = client
@ -242,3 +247,62 @@ async fn dropped() {
let rsp = client.transact(&create, CLTRID).await.unwrap();
assert_eq!(rsp.result.code, ResultCode::CommandCompletedSuccessfully);
}
#[tokio::test]
async fn poll_with_extensions() {
let _guard = log_to_stdout();
struct FakeConnector;
#[async_trait]
impl Connector for FakeConnector {
type Connection = tokio_test::io::Mock;
async fn connect(&self, _: Duration) -> Result<Self::Connection, Error> {
Ok(build_stream(&[
"response/greeting.xml",
"request/login.xml",
"response/login.xml",
"request/poll/poll.xml",
"response/extensions/change_poll/urs_lock_after.xml",
])
.build())
}
}
let mut client =
EppClient::<_, ChangePoll>::new(FakeConnector, "test".into(), Duration::from_secs(5))
.await
.unwrap();
assert_eq!(client.xml_greeting(), xml("response/greeting.xml"));
let rsp = client
.transact(
&Login::new(
"username",
"password",
Some("new-password"),
Some(&["http://schema.ispapi.net/epp/xml/keyvalue-1.0"]),
),
CLTRID,
)
.await
.unwrap();
assert_eq!(rsp.result.code, ResultCode::CommandCompletedSuccessfully);
let rsp = client.transact(&Poll, CLTRID).await.unwrap();
assert_eq!(
rsp.result.code,
ResultCode::CommandCompletedSuccessfullyAckToDequeue
);
let data = rsp.res_data().unwrap();
let data = match data {
PollData::DomainInfo(info_data) => info_data,
_ => panic!("expected domain:infData"),
};
assert_eq!(data.name, "domain.example");
let ext = rsp.connection_extension().unwrap();
assert_eq!(ext.case_id.as_ref().unwrap().id, "urs123");
}