From 2188df56c349013cb3a601eadba523815e3a27db Mon Sep 17 00:00:00 2001 From: Rudi Floren Date: Thu, 5 Dec 2024 19:22:26 +0100 Subject: [PATCH] add RFC8590 implementation Implements the Change Poll Extension for the Extensible Provisioning Protocol. Test cases are taken from the spec. --- src/extensions/change_poll.rs | 359 ++++++++++++++++++ src/lib.rs | 1 + src/poll.rs | 5 +- .../change_poll/autopurge_before.xml | 40 ++ .../change_poll/custom_sync_after.xml | 47 +++ .../extensions/change_poll/delete_before.xml | 39 ++ .../extensions/change_poll/update_after.xml | 47 +++ .../extensions/change_poll/urs_lock_after.xml | 50 +++ .../change_poll/urs_lock_before.xml | 46 +++ 9 files changed, 633 insertions(+), 1 deletion(-) create mode 100644 src/extensions/change_poll.rs create mode 100644 tests/resources/response/extensions/change_poll/autopurge_before.xml create mode 100644 tests/resources/response/extensions/change_poll/custom_sync_after.xml create mode 100644 tests/resources/response/extensions/change_poll/delete_before.xml create mode 100644 tests/resources/response/extensions/change_poll/update_after.xml create mode 100644 tests/resources/response/extensions/change_poll/urs_lock_after.xml create mode 100644 tests/resources/response/extensions/change_poll/urs_lock_before.xml diff --git a/src/extensions/change_poll.rs b/src/extensions/change_poll.rs new file mode 100644 index 0000000..a266a2e --- /dev/null +++ b/src/extensions/change_poll.rs @@ -0,0 +1,359 @@ +//! Types for the EPP change poll extention +//! +//! As described in RFC8590: [Change Poll Extension for the Extensible Provisioning Protocol (EPP)](https://www.rfc-editor.org/rfc/rfc8590.html). +//! Tests cases in `tests/resources/response/extensions/changepoll`` are taken from the RFC. + +use std::borrow::Cow; + +use instant_xml::{Error, FromXml, ToXml}; + +use crate::{ + poll::Poll, + request::{Extension, Transaction}, +}; + +pub const XMLNS: &str = "urn:ietf:params:xml:ns:changePoll-1.0"; + +impl Transaction> for Poll {} + +impl Extension for ChangePoll<'_> { + type Response = ChangePoll<'static>; +} + +/// Type for EPP XML `` extension +/// +/// Attributes associated with the change +#[derive(Debug, FromXml, ToXml)] +#[xml(rename = "changeData", ns(XMLNS))] +pub struct ChangePoll<'a> { + /// Transform operation executed on the object + pub operation: Operation<'a>, + /// Date and time when the operation was executed + pub date: Cow<'a, str>, + /// Server transaction identifier of the operation + #[xml(rename = "svTRID")] + pub server_tr_id: Cow<'a, str>, + /// Who executed the operation + pub who: Cow<'a, str>, + /// Case identifier associated with the operation + pub case_id: Option>, + /// Reason for executing the operation + pub reason: Option, + /// Enumerated state of the object in the poll message + #[xml(attribute)] + // todo: State should utilize the Default impl, + // but instant-xml does not support it yet. + state: Option, +} + +impl ChangePoll<'_> { + /// State reflects if the `infData` describes the object before or after the operation + pub fn state(&self) -> State { + self.state.unwrap_or_default() + } +} + +/// Transform operation type for `` +// todo: Allow struct enum variants with #[xml(attribute, rename = "op")] in instant-xml, +// to make this struct more ergonomic. +#[derive(Debug, FromXml, ToXml)] +#[xml(rename = "operation", ns(XMLNS))] +pub struct Operation<'a> { + /// Custom value for`OperationKind::Custom` + #[xml(attribute, rename = "op")] + op: Option>, + /// The operation + #[xml(direct)] + kind: OperationType, +} + +impl Operation<'_> { + pub fn kind(&self) -> Result { + Ok(match self.kind { + OperationType::Create => OperationKind::Create, + OperationType::Delete => OperationKind::Delete, + OperationType::Renew => OperationKind::Renew, + OperationType::Transfer => OperationKind::Transfer, + OperationType::Update => OperationKind::Update, + OperationType::Restore => OperationKind::Restore, + OperationType::AutoRenew => OperationKind::AutoRenew, + OperationType::AutoDelete => OperationKind::AutoDelete, + OperationType::AutoPurge => OperationKind::AutoPurge, + OperationType::Custom => match self.op.as_deref() { + Some(op) => OperationKind::Custom(op), + None => { + return Err(Error::Other( + "invariant error: Missing op attribute for custom operation".to_string(), + )) + } + }, + }) + } +} + +/// Enumerated list of operations +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum OperationKind<'a> { + Create, + Delete, + Renew, + Transfer, + Update, + Restore, + AutoRenew, + AutoDelete, + AutoPurge, + Custom(&'a str), +} + +/// Internal Enumerated list of operations, with extensibility via "custom" +// See todo on `Operation` struct for reason why this is internal only. +#[derive(Debug, Copy, Clone, FromXml, ToXml)] +#[xml(scalar, rename_all = "camelCase", ns(XMLNS))] +enum OperationType { + Create, + Delete, + Renew, + Transfer, + Update, + Restore, + AutoRenew, + AutoDelete, + AutoPurge, + Custom, +} + +/// Case identifier type for `` +// todo: Allow struct enum variants with #[xml(attribute, rename = "op")] in instant-xml, +// to make this struct more ergonomic. +#[derive(Debug, FromXml, ToXml)] +#[xml(rename = "caseId", ns(XMLNS))] +pub struct CaseIdentifier<'a> { + #[xml(attribute, rename = "type")] + id_type: CaseIdentifierType, + #[xml(attribute)] + name: Option>, + #[xml(direct)] + pub id: Cow<'a, str>, +} + +impl CaseIdentifier<'_> { + pub fn kind(&self) -> Result { + Ok(match self.id_type { + CaseIdentifierType::Udrp => CaseIdentifierKind::Udrp, + CaseIdentifierType::Urs => CaseIdentifierKind::Urs, + CaseIdentifierType::Custom => match self.name.as_deref() { + Some(name) => CaseIdentifierKind::Custom(name), + None => { + return Err(Error::Other( + "invariant error: Missing name attribute for custom case identifier" + .to_string(), + )) + } + }, + }) + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum CaseIdentifierKind<'a> { + Udrp, + Urs, + Custom(&'a str), +} + +/// Internal enumerated list of case identifier types +// See todo on `CaseIdentifier` struct for reason why this is internal only. +#[derive(Debug, Copy, Clone, FromXml, ToXml)] +#[xml(scalar, rename_all = "camelCase")] +enum CaseIdentifierType { + Udrp, + Urs, + Custom, +} + +/// Reason type for `` +/// +/// A human-readable message that describes the reason for the encapsulating element. +/// The language of the response is identified via the "lang" attribute. +/// +/// Schema defined in the `eppcom-1.0` XML schema +// todo: while this is defined in `eppcom` schema, it is used with different +// namespaces in additional specs (for example in RFC8590). +// Currently, instant-xml strongly ties namespaces to schemas and does not allow +// a way out of it for this particular case. +#[derive(Debug, Eq, FromXml, PartialEq, ToXml)] +#[xml(rename = "reason", ns(XMLNS))] +pub struct Reason { + /// The language of the response. If not specified, assume "en" (English). + #[xml(attribute)] + pub lang: Option, + #[xml(direct)] + pub inner: String, +} + +/// Enumerated state of the object in the poll message +#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, FromXml, ToXml)] +#[xml(scalar, rename_all = "camelCase")] +pub enum State { + Before, + #[default] + After, +} + +#[cfg(test)] +mod tests { + use super::*; + 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::( + "response/extensions/change_poll/urs_lock_before.xml", + ); + + assert_eq!( + object.result.code, + ResultCode::CommandCompletedSuccessfullyAckToDequeue + ); + assert_eq!( + object.result.message, + "Command completed successfully; ack to dequeue" + ); + + assert_eq!(object.extension().unwrap().state.unwrap(), State::Before); + 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(), + CaseIdentifierKind::Urs + ); + assert_eq!( + object.extension().unwrap().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); + } + + #[test] + fn urs_lock_after() { + let object = response_from_file_with_ext::( + "response/extensions/change_poll/urs_lock_after.xml", + ); + + assert_eq!( + object.result.code, + ResultCode::CommandCompletedSuccessfullyAckToDequeue + ); + assert_eq!( + object.result.message, + "Command completed successfully; ack to dequeue" + ); + assert_eq!(object.extension().unwrap().state.unwrap(), State::After); + + assert_eq!(object.tr_ids.client_tr_id.unwrap(), CLTRID); + assert_eq!(object.tr_ids.server_tr_id, SVTRID); + } + + #[test] + fn custom_sync_after() { + let object = response_from_file_with_ext::( + "response/extensions/change_poll/custom_sync_after.xml", + ); + + assert_eq!( + object.result.code, + ResultCode::CommandCompletedSuccessfullyAckToDequeue + ); + assert_eq!( + object.result.message, + "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" + ); + + assert_eq!(object.tr_ids.client_tr_id.unwrap(), CLTRID); + assert_eq!(object.tr_ids.server_tr_id, SVTRID); + } + + #[test] + fn delete_before() { + let object = response_from_file_with_ext::( + "response/extensions/change_poll/delete_before.xml", + ); + + assert_eq!( + object.result.code, + ResultCode::CommandCompletedSuccessfullyAckToDequeue + ); + assert_eq!( + object.result.message, + "Command completed successfully; ack to dequeue" + ); + + assert_eq!(object.tr_ids.client_tr_id.unwrap(), CLTRID); + assert_eq!(object.tr_ids.server_tr_id, SVTRID); + } + + #[test] + fn autopurge_before() { + let object = response_from_file_with_ext::( + "response/extensions/change_poll/autopurge_before.xml", + ); + + assert_eq!( + object.result.code, + ResultCode::CommandCompletedSuccessfullyAckToDequeue + ); + assert_eq!( + object.result.message, + "Command completed successfully; ack to dequeue" + ); + + assert_eq!(object.tr_ids.client_tr_id.unwrap(), CLTRID); + assert_eq!(object.tr_ids.server_tr_id, SVTRID); + } + + #[test] + fn update_after() { + let object = response_from_file_with_ext::( + "response/extensions/change_poll/update_after.xml", + ); + + assert_eq!( + object.result.code, + ResultCode::CommandCompletedSuccessfullyAckToDequeue + ); + assert_eq!( + object.result.message, + "Command completed successfully; ack to dequeue" + ); + + assert_eq!(object.tr_ids.client_tr_id.unwrap(), CLTRID); + assert_eq!(object.tr_ids.server_tr_id, SVTRID); + } +} diff --git a/src/lib.rs b/src/lib.rs index 753b610..a638503 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -51,6 +51,7 @@ pub mod response; pub mod xml; pub mod extensions { + pub mod change_poll; pub mod consolidate; pub mod frnic; pub mod low_balance; diff --git a/src/poll.rs b/src/poll.rs index 9158396..eb2f9f4 100644 --- a/src/poll.rs +++ b/src/poll.rs @@ -1,6 +1,7 @@ use instant_xml::{FromXml, ToXml}; use crate::common::{NoExtension, EPP_XMLNS}; +use crate::domain; use crate::domain::transfer::TransferData; use crate::extensions::low_balance::LowBalance; use crate::extensions::rgp::poll::RgpPollData; @@ -61,12 +62,14 @@ impl ToXml for Ack<'_> { // Response -/// Type that represents the `` tag for message poll response +/// Type that represents the `` tag for message poll response #[derive(Debug, FromXml)] #[xml(forward)] pub enum PollData { /// Data under the `` tag DomainTransfer(TransferData), + /// Data under the `` tag + DomainInfo(domain::InfoData), /// Data under the `` tag HostInfo(host::InfoData), /// Data under the `` tag diff --git a/tests/resources/response/extensions/change_poll/autopurge_before.xml b/tests/resources/response/extensions/change_poll/autopurge_before.xml new file mode 100644 index 0000000..644edef --- /dev/null +++ b/tests/resources/response/extensions/change_poll/autopurge_before.xml @@ -0,0 +1,40 @@ + + + + + + Command completed successfully; ack to dequeue + + + 2013-10-22T14:25:57.0Z + Registry purged domain with pendingDelete status. + + + + domain.example + EXAMPLE1-REP + + ClientX + + + + + autoPurge + 2013-10-22T14:25:57.0Z + 12345-XYZ + Batch + Past pendingDelete 5 day period + + + + cltrid:1626454866 + RO-6879-1627224678242975 + + + diff --git a/tests/resources/response/extensions/change_poll/custom_sync_after.xml b/tests/resources/response/extensions/change_poll/custom_sync_after.xml new file mode 100644 index 0000000..a3d3a84 --- /dev/null +++ b/tests/resources/response/extensions/change_poll/custom_sync_after.xml @@ -0,0 +1,47 @@ + + + + + + Command completed successfully; ack to dequeue + + + 2013-10-22T14:25:57.0Z + Registry initiated Sync of Domain Expiration Date + + + + domain.example + EXAMPLE1-REP + + jd1234 + sh8013 + sh8013 + ClientX + ClientY + 2012-04-03T22:00:00.0Z + ClientZ + 2013-10-22T14:25:57.0Z + 2014-04-03T22:00:00.0Z + + + + + custom + 2013-10-22T14:25:57.0Z + 12345-XYZ + CSR + Customer sync request + + + + cltrid:1626454866 + RO-6879-1627224678242975 + + + \ No newline at end of file diff --git a/tests/resources/response/extensions/change_poll/delete_before.xml b/tests/resources/response/extensions/change_poll/delete_before.xml new file mode 100644 index 0000000..811ddba --- /dev/null +++ b/tests/resources/response/extensions/change_poll/delete_before.xml @@ -0,0 +1,39 @@ + + + + + + Command completed successfully; ack to dequeue + + + 2013-10-22T14:25:57.0Z + Registry initiated delete of domain resulting in immediate purge. + + + + domain.example + EXAMPLE1-REP + ClientX + + + + + delete + 2013-10-22T14:25:57.0Z + 12345-XYZ + ClientZ + Court order + + + + cltrid:1626454866 + RO-6879-1627224678242975 + + + \ No newline at end of file diff --git a/tests/resources/response/extensions/change_poll/update_after.xml b/tests/resources/response/extensions/change_poll/update_after.xml new file mode 100644 index 0000000..ea80919 --- /dev/null +++ b/tests/resources/response/extensions/change_poll/update_after.xml @@ -0,0 +1,47 @@ + + + + + + Command completed successfully; ack to dequeue + + + 2013-10-22T14:25:57.0Z + Registry initiated update of host. + + + + ns1.domain.example + NS1_EXAMPLE1-REP + + + + 192.0.2.2 + 2001:db8:0:0:1:0:0:1 + ClientX + ClientY + 2012-04-03T22:00:00.0Z + ClientY + 2013-10-22T14:25:57.0Z + + + + + update + 2013-10-22T14:25:57.0Z + 12345-XYZ + ClientZ + Host Lock + + + + cltrid:1626454866 + RO-6879-1627224678242975 + + + diff --git a/tests/resources/response/extensions/change_poll/urs_lock_after.xml b/tests/resources/response/extensions/change_poll/urs_lock_after.xml new file mode 100644 index 0000000..18c0e6c --- /dev/null +++ b/tests/resources/response/extensions/change_poll/urs_lock_after.xml @@ -0,0 +1,50 @@ + + + + + + Command completed successfully; ack to dequeue + + + 2013-10-22T14:25:57.0Z + Registry initiated update of domain. + + + + domain.example + EXAMPLE1-REP + + + + jd1234 + sh8013 + sh8013 + ClientX + ClientY + 2012-04-03T22:00:00.0Z + ClientZ + 2013-10-22T14:25:57.0Z + 2014-04-03T22:00:00.0Z + + + + + update + 2013-10-22T14:25:57.0Z + 12345-XYZ + URS Admin + urs123 + URS Lock + + + + cltrid:1626454866 + RO-6879-1627224678242975 + + + \ No newline at end of file diff --git a/tests/resources/response/extensions/change_poll/urs_lock_before.xml b/tests/resources/response/extensions/change_poll/urs_lock_before.xml new file mode 100644 index 0000000..a9ccda8 --- /dev/null +++ b/tests/resources/response/extensions/change_poll/urs_lock_before.xml @@ -0,0 +1,46 @@ + + + + + + Command completed successfully; ack to dequeue + + + 2013-10-22T14:25:57.0Z + Registry initiated update of domain. + + + + domain.example + EXAMPLE1-REP + + jd1234 + sh8013 + sh8013 + ClientX + ClientY + 2012-04-03T22:00:00.0Z + 2014-04-03T22:00:00.0Z + + + + + update + 2013-10-22T14:25:57.0Z + 12345-XYZ + URS Admin + urs123 + URS Lock + + + + cltrid:1626454866 + RO-6879-1627224678242975 + + + \ No newline at end of file