diff --git a/Cargo.toml b/Cargo.toml index 7e9ffee..cab0959 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ rustls = ["tokio-rustls", "rustls-pki-types", "rustls-native-certs"] async-trait = "0.1.52" celes = "2.1" chrono = { version = "0.4.23", features = ["serde"] } -instant-xml = { version = "0.5", features = ["chrono"] } +instant-xml = { version = "0.5.1", features = ["chrono"] } rustls-native-certs = { version = "0.8", optional = true } rustls-pki-types = { version = "1", optional = true } serde = { version = "1.0", features = ["derive"] } diff --git a/src/extensions/secdns.rs b/src/extensions/secdns.rs index d2ccfbe..958e254 100644 --- a/src/extensions/secdns.rs +++ b/src/extensions/secdns.rs @@ -1,7 +1,7 @@ //! DNS security extensions mapping //! //! As described in [RFC 5910](https://www.rfc-editor.org/rfc/rfc5910) -use instant_xml::{Error, Id, Serializer, ToXml}; +use instant_xml::{Error, FromXml, Id, Serializer, ToXml}; use std::borrow::Cow; use std::fmt::Write; use std::time::Duration; @@ -28,7 +28,7 @@ impl<'a> From<&'a [DsDataType<'a>]> for CreateData<'a> { Self { data: DsOrKeyType { maximum_signature_lifetime: None, - data: DsOrKeyData::DsData(s), + data: DsOrKeyData::DsData(Cow::Borrowed(s)), }, } } @@ -39,7 +39,7 @@ impl<'a> From<&'a [KeyDataType<'a>]> for CreateData<'a> { Self { data: DsOrKeyType { maximum_signature_lifetime: None, - data: DsOrKeyData::KeyData(s), + data: DsOrKeyData::KeyData(Cow::Borrowed(s)), }, } } @@ -49,8 +49,10 @@ impl<'a> From<(Duration, &'a [DsDataType<'a>])> for CreateData<'a> { fn from((maximum_signature_lifetime, data): (Duration, &'a [DsDataType<'a>])) -> Self { Self { data: DsOrKeyType { - maximum_signature_lifetime: Some(maximum_signature_lifetime), - data: DsOrKeyData::DsData(data), + maximum_signature_lifetime: Some(MaximumSignatureLifeTime( + maximum_signature_lifetime, + )), + data: DsOrKeyData::DsData(Cow::Borrowed(data)), }, } } @@ -60,51 +62,96 @@ impl<'a> From<(Duration, &'a [KeyDataType<'a>])> for CreateData<'a> { fn from((maximum_signature_lifetime, data): (Duration, &'a [KeyDataType<'a>])) -> Self { Self { data: DsOrKeyType { - maximum_signature_lifetime: Some(maximum_signature_lifetime), - data: DsOrKeyData::KeyData(data), + maximum_signature_lifetime: Some(MaximumSignatureLifeTime( + maximum_signature_lifetime, + )), + data: DsOrKeyData::KeyData(Cow::Borrowed(data)), }, } } } +impl Transaction for crate::domain::info::DomainInfo<'_> {} + +#[derive(Debug, ToXml)] +pub struct InfoData; + +#[derive(Debug, FromXml)] +#[xml(rename = "infData", ns(XMLNS))] +pub struct InfoDataResponse<'a> { + pub data: DsOrKeyType<'a>, +} + +impl Extension for InfoData { + type Response = InfoDataResponse<'static>; +} + /// Struct supporting either the `dsData` or the `keyData` interface. -#[derive(Debug)] +#[derive(Debug, ToXml, FromXml)] +#[xml(transparent)] pub struct DsOrKeyType<'a> { - maximum_signature_lifetime: Option, + maximum_signature_lifetime: Option, data: DsOrKeyData<'a>, } -impl ToXml for DsOrKeyType<'_> { - fn serialize( +#[derive(Debug, PartialEq)] +pub struct MaximumSignatureLifeTime(pub Duration); + +impl MaximumSignatureLifeTime { + const ELEMENT_NAME: &str = "maxSigLife"; +} + +impl ToXml for MaximumSignatureLifeTime { + fn serialize( &self, _: Option>, - serializer: &mut Serializer<'_, W>, + serializer: &mut Serializer, ) -> Result<(), Error> { - if let Some(maximum_signature_lifetime) = self.maximum_signature_lifetime { - let nc_name = "maxSigLife"; - let prefix = serializer.write_start(nc_name, XMLNS)?; - serializer.end_start()?; - maximum_signature_lifetime - .as_secs() - .serialize(None, serializer)?; - serializer.write_close(prefix, nc_name)?; + let prefix = serializer.write_start(Self::ELEMENT_NAME, XMLNS)?; + serializer.end_start()?; + + self.0.as_secs().serialize(None, serializer)?; + + serializer.write_close(prefix, Self::ELEMENT_NAME) + } +} + +impl<'xml> FromXml<'xml> for MaximumSignatureLifeTime { + fn matches(id: Id<'_>, _: Option>) -> bool { + id == instant_xml::Id { + ns: XMLNS, + name: Self::ELEMENT_NAME, } - match &self.data { - DsOrKeyData::DsData(data) => data.serialize(None, serializer)?, - DsOrKeyData::KeyData(data) => data.serialize(None, serializer)?, + } + + fn deserialize<'cx>( + into: &mut Self::Accumulator, + field: &'static str, + deserializer: &mut instant_xml::Deserializer<'cx, 'xml>, + ) -> Result<(), Error> { + let mut value = as FromXml<'xml>>::Accumulator::default(); + + instant_xml::from_xml_str(value.get_mut(), field, deserializer)?; + + if let Some(duration_seconds) = value.get_mut() { + *into = Some(Self(Duration::from_secs(*duration_seconds))); } + Ok(()) } + + type Accumulator = Option; + const KIND: instant_xml::Kind = instant_xml::Kind::Scalar; } -#[derive(Debug, ToXml)] +#[derive(Debug, ToXml, FromXml)] #[xml(forward)] pub enum DsOrKeyData<'a> { - DsData(&'a [DsDataType<'a>]), - KeyData(&'a [KeyDataType<'a>]), + DsData(Cow<'a, [DsDataType<'a>]>), + KeyData(Cow<'a, [KeyDataType<'a>]>), } -#[derive(Debug, ToXml)] +#[derive(Debug, Clone, ToXml, FromXml)] #[xml(rename = "dsData", ns(XMLNS))] pub struct DsDataType<'a> { #[xml(rename = "keyTag")] @@ -162,16 +209,21 @@ impl From for u8 { } } -impl ToXml for DigestAlgorithm { - fn serialize( - &self, - id: Option>, - serializer: &mut Serializer<'_, W>, - ) -> Result<(), Error> { - u8::from(*self).serialize(id, serializer) +impl From for DigestAlgorithm { + fn from(n: u8) -> Self { + match n { + 1 => Self::Sha1, + 2 => Self::Sha256, + 3 => Self::Gost, + 4 => Self::Sha384, + _ => Self::Other(n), + } } } +crate::xml::from_scalar!(DigestAlgorithm, u8); +crate::xml::to_scalar!(DigestAlgorithm, u8); + /// Algorithm identifies the public key's cryptographic algorithm /// #[derive(Clone, Copy, Debug)] @@ -245,17 +297,36 @@ impl From for u8 { } } -impl ToXml for Algorithm { - fn serialize( - &self, - id: Option>, - serializer: &mut Serializer<'_, W>, - ) -> Result<(), Error> { - u8::from(*self).serialize(id, serializer) +impl From for Algorithm { + fn from(n: u8) -> Self { + match n { + 0 => Self::Delete, + 1 => Self::RsaMd5, + 2 => Self::Dh, + 3 => Self::Dsa, + 4 => Self::Ecc, + 5 => Self::RsaSha1, + 6 => Self::DsaNsec3Sha1, + 7 => Self::RsaSha1Nsec3Sha1, + 8 => Self::RsaSha256, + 10 => Self::RsaSha512, + 12 => Self::EccGost, + 13 => Self::EcdsaP256Sha256, + 14 => Self::EcdsaP384Sha384, + 15 => Self::Ed25519, + 16 => Self::Ed448, + 252 => Self::Indirect, + 253 => Self::PrivateDns, + 254 => Self::PrivateOid, + _ => Self::Other(n), + } } } -#[derive(Debug, ToXml)] +crate::xml::from_scalar!(Algorithm, u8); +crate::xml::to_scalar!(Algorithm, u8); + +#[derive(Debug, Clone, ToXml, FromXml)] #[xml(rename = "keyData", ns(XMLNS))] pub struct KeyDataType<'a> { flags: Flags, @@ -282,7 +353,7 @@ impl<'a> KeyDataType<'a> { } } -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, PartialEq)] pub struct Flags { /// Zone Key flag. If `true` then the DNSKEY record holds a DNS /// zone key. If `false` then the DNSKEY record holds some other @@ -293,29 +364,36 @@ pub struct Flags { secure_entry_point: bool, } +impl Flags { + const ZONE_KEY: u16 = 0b1_0000_0000; + const SECURE_ENDPOINT: u16 = 0x1; +} + impl From for u16 { fn from(flags: Flags) -> Self { let mut res = 0; if flags.zone_key { - res |= 0b1_0000_0000; + res |= Flags::ZONE_KEY; } if flags.secure_entry_point { - res |= 0x1; + res |= Flags::SECURE_ENDPOINT; } res } } -impl ToXml for Flags { - fn serialize( - &self, - id: Option>, - serializer: &mut Serializer<'_, W>, - ) -> Result<(), Error> { - u16::from(*self).serialize(id, serializer) +impl From for Flags { + fn from(n: u16) -> Self { + Self { + zone_key: (n | Self::ZONE_KEY == n), + secure_entry_point: (n | Self::SECURE_ENDPOINT == n), + } } } +crate::xml::from_scalar!(Flags, u16); +crate::xml::to_scalar!(Flags, u16); + /// `Flags` for a zone signing key. pub const FLAGS_DNS_ZONE_KEY: Flags = Flags { zone_key: true, @@ -358,31 +436,84 @@ impl From for u8 { } } -impl ToXml for Protocol { - fn serialize( - &self, - id: Option>, - serializer: &mut Serializer<'_, W>, - ) -> Result<(), Error> { - u8::from(*self).serialize(id, serializer) +impl From for Protocol { + fn from(n: u8) -> Self { + match n { + 1 => Self::Tls, + 2 => Self::Email, + 3 => Self::Dnssec, + 4 => Self::Ipsec, + 255 => Self::All, + _ => Self::Other(n), + } } } +crate::xml::from_scalar!(Protocol, u8); +crate::xml::to_scalar!(Protocol, u8); + #[cfg(test)] mod tests { use super::*; - use crate::domain::{self, Period, PeriodLength}; - use crate::tests::assert_serialized; + use crate::domain::{self, DomainInfo, Period, PeriodLength}; + use crate::tests::{assert_serialized, response_from_file_with_ext}; - #[test] - fn create_ds_data_interface() { - let ds_data = [DsDataType::new( + mod maximum_signature_lifetime { + use super::*; + + #[test] + fn serialization() { + let v = Some(MaximumSignatureLifeTime(Duration::from_secs(10))); + let xml = r#"10"#; + + assert_eq!(xml, instant_xml::to_string(&v).unwrap()); + assert_eq!(v, instant_xml::from_str(xml).unwrap()); + } + } + + mod flags { + use super::*; + + #[test] + fn bijectivity() { + assert_bijectivity(true, true); + assert_bijectivity(true, false); + assert_bijectivity(false, false); + assert_bijectivity(false, true); + + fn assert_bijectivity(zone_key: bool, secure_entry_point: bool) { + let flags = Flags { + zone_key, + secure_entry_point, + }; + + assert_eq!(flags, u16::from(flags).into()) + } + } + } + + fn mock_key_data() -> KeyDataType<'static> { + KeyDataType::new( + FLAGS_DNS_ZONE_KEY_SEP, + Protocol::Dnssec, + Algorithm::RsaMd5, + "AQPJ////4Q==", + ) + } + + fn mock_ds_data(key_data: Option>) -> DsDataType<'static> { + DsDataType::new( 12345, Algorithm::Dsa, DigestAlgorithm::Sha1, "49FD46E6C4B45C55D4AC", - None, - )]; + key_data, + ) + } + + #[test] + fn create_ds_data_interface() { + let ds_data = [mock_ds_data(None)]; let extension = CreateData::from((Duration::from_secs(604800), ds_data.as_ref())); let ns = [ domain::HostInfo::Obj(domain::HostObj { @@ -418,19 +549,8 @@ mod tests { #[test] fn create_ds_and_key_data_interface() { - let key_data = KeyDataType::new( - FLAGS_DNS_ZONE_KEY_SEP, - Protocol::Dnssec, - Algorithm::Dsa, - "AQPJ////4Q==", - ); - let ds_data = [DsDataType::new( - 12345, - Algorithm::Dsa, - DigestAlgorithm::Sha1, - "49FD46E6C4B45C55D4AC", - Some(key_data), - )]; + let ds_data = [mock_ds_data(Some(mock_key_data()))]; + let extension = CreateData::from((Duration::from_secs(604800), ds_data.as_ref())); let ns = [ domain::HostInfo::Obj(domain::HostObj { @@ -466,12 +586,7 @@ mod tests { #[test] fn create_key_data_interface() { - let key_data = [KeyDataType::new( - FLAGS_DNS_ZONE_KEY_SEP, - Protocol::Dnssec, - Algorithm::RsaMd5, - "AQPJ////4Q==", - )]; + let key_data = [mock_key_data()]; let extension = CreateData::from(key_data.as_ref()); let ns = [ domain::HostInfo::Obj(domain::HostObj { @@ -504,4 +619,18 @@ mod tests { (&object, &extension), ); } + + #[test] + fn info_ds_data_interface() { + response_from_file_with_ext::( + "response/extensions/secdns_info_ds.xml", + ); + } + + #[test] + fn info_key_data_interface() { + response_from_file_with_ext::( + "response/extensions/secdns_info_key.xml", + ); + } } diff --git a/src/xml.rs b/src/xml.rs index 48a335e..481f880 100644 --- a/src/xml.rs +++ b/src/xml.rs @@ -27,3 +27,51 @@ pub(crate) fn deserialize(xml: &str) -> Result { pub(crate) struct Epp { pub(crate) data: T, } + +macro_rules! from_scalar { + ($name:ty, $scalar:tt) => { + impl<'xml> ::instant_xml::FromXml<'xml> for $name { + fn matches(id: ::instant_xml::Id<'_>, field: Option<::instant_xml::Id<'_>>) -> bool { + match field { + Some(field) => id == field, + None => false, + } + } + + fn deserialize<'cx>( + into: &mut Self::Accumulator, + field: &'static str, + deserializer: &mut ::instant_xml::Deserializer<'cx, 'xml>, + ) -> Result<(), ::instant_xml::Error> { + let mut value = None; + + $scalar::deserialize(&mut value, field, deserializer)?; + + if let Some(value) = value { + *into = Some(Self::from(value)); + } + + Ok(()) + } + + type Accumulator = Option; + const KIND: ::instant_xml::Kind = ::instant_xml::Kind::Scalar; + } + }; +} +pub(crate) use from_scalar; + +macro_rules! to_scalar { + ($name:ty, $scalar:tt) => { + impl ::instant_xml::ToXml for $name { + fn serialize( + &self, + id: Option>, + serializer: &mut Serializer<'_, W>, + ) -> Result<(), Error> { + $scalar::from(*self).serialize(id, serializer) + } + } + }; +} +pub(crate) use to_scalar; diff --git a/tests/resources/request/extensions/secdns_create_ds_key.xml b/tests/resources/request/extensions/secdns_create_ds_key.xml index 6e7e310..7cfc511 100644 --- a/tests/resources/request/extensions/secdns_create_ds_key.xml +++ b/tests/resources/request/extensions/secdns_create_ds_key.xml @@ -28,7 +28,7 @@ 257 3 - 3 + 1 AQPJ////4Q== diff --git a/tests/resources/response/extensions/secdns_info_ds.xml b/tests/resources/response/extensions/secdns_info_ds.xml new file mode 100644 index 0000000..3faf514 --- /dev/null +++ b/tests/resources/response/extensions/secdns_info_ds.xml @@ -0,0 +1,55 @@ + + + + + Command completed successfully + + + + example.com + EXAMPLE1-REP + + jd1234 + sh8013 + sh8013 + + ns1.example.com + ns2.example.com + + ns1.example.com + ns2.example.com + ClientX + ClientY + 1999-04-03T22:00:00.0Z + ClientX + 1999-12-03T09:00:00.0Z + 2005-04-03T22:00:00.0Z + 2000-04-08T09:00:00.0Z + + 2fooBAR + + + + + + 604800 + + 12345 + 3 + 1 + 49FD46E6C4B45C55D4AC + + 257 + 3 + 1 + AQPJ////4Q== + + + + + + ABC-12345 + 54322-XYZ + + + diff --git a/tests/resources/response/extensions/secdns_info_key.xml b/tests/resources/response/extensions/secdns_info_key.xml new file mode 100644 index 0000000..dd351a6 --- /dev/null +++ b/tests/resources/response/extensions/secdns_info_key.xml @@ -0,0 +1,48 @@ + + + + + Command completed successfully + + + + example.com + EXAMPLE1-REP + + jd1234 + sh8013 + sh8013 + + ns1.example.com + ns2.example.com + + ns1.example.com + ns2.example.com + ClientX + ClientY + 1999-04-03T22:00:00.0Z + ClientX + 1999-12-03T09:00:00.0Z + 2005-04-03T22:00:00.0Z + 2000-04-08T09:00:00.0Z + + 2fooBAR + + + + + + + 257 + 3 + 1 + AQPJ////4Q== + + + + + ABC-12345 + 54322-XYZ + + +