mirror of
https://github.com/NLnetLabs/domain.git
synced 2026-09-28 04:34:55 +02:00
Implement support for various record types (#569)
* Implement support for SSHFP records * Implement support for OPENPGPKEY records * Implement support for TLSA records * Implement support for IPSECKEY records * Add tests for IPSECKEY * Add key length checks for IPSECKEY * Implement support for SVCB and HTTPS records in zonefiles * Fix reading SvcParamKey without value * Derive ipseckey gateway type from gateway * Add SVCB test vectors from RFC 9460 * Disable SVCB alpn escaping Implementing evaluating the escaped backslash and comma would require access to a mutable octet sequence, but instead of implementing that right now, we can just forbid escapes in alpn as permitted by the RFC.
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
//! IPSECKEY IANA parameters.
|
||||
//!
|
||||
//! The values of these types don't officially have an IANA assigned name and
|
||||
//! mnemonic. For ease of use, we define them here anyway.
|
||||
|
||||
//------------ IpseckeyAlgorithm ---------------------------------------------
|
||||
|
||||
int_enum! {
|
||||
/// IPSECKEY Algorithms.
|
||||
///
|
||||
/// This type identifies the public key's cryptographic algorithm of the
|
||||
/// [IPSECKEY].
|
||||
///
|
||||
/// For the currently registered values see the [IANA registration]. This
|
||||
/// type is complete as of 2025-09-09.
|
||||
///
|
||||
/// [IPSECKEY]: ../../../rdata/ipseckey/index.html
|
||||
/// [IANA registration]: https://www.iana.org/assignments/ipseckey-rr-parameters/ipseckey-rr-parameters.xhtml#ipseckey-rr-parameters-1
|
||||
=>
|
||||
IpseckeyAlgorithm, u8;
|
||||
|
||||
/// Specified that no Public key is present.
|
||||
(NONE => 0, "NONE")
|
||||
|
||||
/// Specified that a DSA Public Key is used.
|
||||
(DSA => 1, "DSA")
|
||||
|
||||
/// Specified that an RSA Public Key is used.
|
||||
(RSA => 2, "RSA")
|
||||
|
||||
/// Specified that an ECDSA Public Key is used.
|
||||
(ECDSA => 3, "ECDSA")
|
||||
|
||||
/// Specified that an EdDSA Public Key is used.
|
||||
(EDDSA => 4, "EdDSA")
|
||||
}
|
||||
|
||||
int_enum_str_decimal!(IpseckeyAlgorithm, u8);
|
||||
int_enum_zonefile_fmt_decimal!(IpseckeyAlgorithm, "ipseckey algorithm");
|
||||
|
||||
//------------ IpseckeyGateway -----------------------------------------------
|
||||
|
||||
int_enum! {
|
||||
/// IPSECKEY Gateway Types.
|
||||
///
|
||||
/// This type indicates the format of the information that is stored in
|
||||
/// the gateway field of the [IPSECKEY].
|
||||
///
|
||||
/// For the currently registered values see the [IANA registration]. This
|
||||
/// type is complete as of 2025-09-09.
|
||||
///
|
||||
/// [IPSECKEY]: ../../../rdata/ipseckey/index.html
|
||||
/// [IANA registration]: https://www.iana.org/assignments/ipseckey-rr-parameters/ipseckey-rr-parameters.xhtml#ipseckey-rr-parameters-2
|
||||
=>
|
||||
IpseckeyGatewayType, u8;
|
||||
|
||||
/// Specified that No gateway is present.
|
||||
(NONE => 0, "NONE")
|
||||
|
||||
/// Specified that A 4-byte IPv4 address is present.
|
||||
(IPV4 => 1, "IPV4")
|
||||
|
||||
/// Specified that A 16-byte IPv6 address is present.
|
||||
(IPV6 => 2, "IPV6")
|
||||
|
||||
/// Specified that A wire-encoded domain name is present.
|
||||
(NAME => 3, "NAME")
|
||||
}
|
||||
|
||||
int_enum_str_decimal!(IpseckeyGatewayType, u8);
|
||||
int_enum_zonefile_fmt_decimal!(IpseckeyGatewayType, "ipseckey gateway type");
|
||||
@@ -28,13 +28,16 @@
|
||||
pub use self::class::Class;
|
||||
pub use self::digestalg::DigestAlgorithm;
|
||||
pub use self::exterr::ExtendedErrorCode;
|
||||
pub use self::ipseckey::{IpseckeyAlgorithm, IpseckeyGatewayType};
|
||||
pub use self::nsec3::Nsec3HashAlgorithm;
|
||||
pub use self::opcode::Opcode;
|
||||
pub use self::opt::OptionCode;
|
||||
pub use self::rcode::{OptRcode, Rcode, TsigRcode};
|
||||
pub use self::rtype::Rtype;
|
||||
pub use self::secalg::SecurityAlgorithm;
|
||||
pub use self::sshfp::{SshfpAlgorithm, SshfpType};
|
||||
pub use self::svcb::SvcParamKey;
|
||||
pub use self::tlsa::{TlsaCertificateUsage, TlsaMatchingType, TlsaSelector};
|
||||
pub use self::zonemd::{ZonemdAlgorithm, ZonemdScheme};
|
||||
|
||||
#[macro_use]
|
||||
@@ -43,11 +46,14 @@ mod macros;
|
||||
pub mod class;
|
||||
pub mod digestalg;
|
||||
pub mod exterr;
|
||||
pub mod ipseckey;
|
||||
pub mod nsec3;
|
||||
pub mod opcode;
|
||||
pub mod opt;
|
||||
pub mod rcode;
|
||||
pub mod rtype;
|
||||
pub mod secalg;
|
||||
pub mod sshfp;
|
||||
pub mod svcb;
|
||||
pub mod tlsa;
|
||||
pub mod zonemd;
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
//! SSHFP IANA parameters.
|
||||
//!
|
||||
//! [RFC 4255]: https://tools.ietf.org/html/rfc4255
|
||||
//! [RFC 6594]: https://tools.ietf.org/html/rfc6594
|
||||
//! [RFC 7479]: https://tools.ietf.org/html/rfc7479
|
||||
//! [RFC 8709]: https://tools.ietf.org/html/rfc8709
|
||||
//!
|
||||
//! The values of these types don't officially have an IANA assigned mnemonic.
|
||||
//! For ease of use, we define them here anyway.
|
||||
|
||||
//------------ SshfpType -----------------------------------------------------
|
||||
|
||||
int_enum! {
|
||||
/// SSHFP fingerprint type.
|
||||
///
|
||||
/// This type selects the digest algorithm used for the fingerprint in the
|
||||
/// [SSHFP] record.
|
||||
///
|
||||
/// For the currently registered values see the [IANA registration]. This
|
||||
/// type is complete as of 2025-09-04.
|
||||
///
|
||||
/// [SSHFP]: ../../../rdata/sshfp/index.html
|
||||
/// [IANA registration]: https://www.iana.org/assignments/dns-sshfp-rr-parameters/dns-sshfp-rr-parameters.xhtml#dns-sshfp-rr-parameters-2
|
||||
=>
|
||||
SshfpType, u8;
|
||||
|
||||
(RESERVED => 0, "Reserved")
|
||||
|
||||
/// Specified that the SHA-1 algorithm is used. [RFC4255]
|
||||
(SHA1 => 1, "SHA-1")
|
||||
|
||||
/// Specified that the SHA-256 algorithm is used. [RFC6594]
|
||||
(SHA256 => 2, "SHA-256")
|
||||
|
||||
}
|
||||
|
||||
int_enum_str_decimal!(SshfpType, u8);
|
||||
int_enum_zonefile_fmt_decimal!(SshfpType, "fingerprint type");
|
||||
|
||||
//------------ SshfpAlgorithm ------------------------------------------------
|
||||
|
||||
int_enum! {
|
||||
/// SSHFP public key algorithms.
|
||||
///
|
||||
/// This type selects the algorithm of the public key associated with the [SSHFP].
|
||||
///
|
||||
/// For the currently registered values see the [IANA registration]. This
|
||||
/// type is complete as of 2025-09-04.
|
||||
///
|
||||
/// [SSHFP]: ../../../rdata/sshfp/index.html
|
||||
/// [IANA registration]: https://www.iana.org/assignments/dns-sshfp-rr-parameters/dns-sshfp-rr-parameters.xhtml#dns-sshfp-rr-parameters-1
|
||||
=>
|
||||
SshfpAlgorithm, u8;
|
||||
|
||||
/// Specified that the Reserved algorithm is used. [RFC4255]
|
||||
(RESERVED => 0, "Reserved")
|
||||
|
||||
/// Specified that the RSA algorithm is used. [RFC4255]
|
||||
(RSA => 1, "RSA")
|
||||
|
||||
/// Specified that the DSA algorithm is used. [RFC4255]
|
||||
(DSA => 2, "DSA")
|
||||
|
||||
/// Specified that the ECDSA algorithm is used. [RFC6594]
|
||||
(ECDSA => 3, "ECDSA")
|
||||
|
||||
/// Specified that the Ed25519 algorithm is used. [RFC7479]
|
||||
(ED25519 => 4, "Ed25519")
|
||||
|
||||
/// Specified that the Ed448 algorithm is used. [RFC8709]
|
||||
(ED448 => 6, "Ed448")
|
||||
}
|
||||
|
||||
int_enum_str_decimal!(SshfpAlgorithm, u8);
|
||||
int_enum_zonefile_fmt_decimal!(SshfpAlgorithm, "public key algorithm");
|
||||
@@ -12,8 +12,12 @@ int_enum! {
|
||||
// https://datatracker.ietf.org/doc/draft-ietf-tls-esni/
|
||||
(ECH => 5, "ech")
|
||||
(IPV6HINT => 6, "ipv6hint")
|
||||
// https://datatracker.ietf.org/doc/draft-ietf-add-svcb-dns/
|
||||
// https://datatracker.ietf.org/doc/rfc9461/
|
||||
(DOHPATH => 7, "dohpath")
|
||||
(OHTTP => 8, "ohttp")
|
||||
// https://datatracker.ietf.org/doc/draft-ietf-tls-key-share-prediction/
|
||||
(TLS_SUPPORTED_GROUPS => 9, "tls-supported-groups")
|
||||
// TODO: docpath https://datatracker.ietf.org/doc/draft-ietf-core-dns-over-coap/
|
||||
}
|
||||
|
||||
int_enum_str_with_prefix!(SvcParamKey, "key", b"key", u16, "unknown key");
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
//! TLSA IANA parameters.
|
||||
|
||||
//------------ TlsaCertificateUsage ------------------------------------------
|
||||
|
||||
int_enum! {
|
||||
/// TLSA Certificate Usage type.
|
||||
///
|
||||
/// This type specifies the provided association that will be used to match the certificate
|
||||
/// presented in the TLS handshake
|
||||
///
|
||||
/// For the currently registered values see the [IANA registration]. This
|
||||
/// type is complete as of 2025-09-04.
|
||||
///
|
||||
/// [TLSA]: ../../../rdata/tlsa/index.html
|
||||
/// [IANA registration]: https://www.iana.org/assignments/dane-parameters/dane-parameters.xhtml#certificate-usages
|
||||
=>
|
||||
TlsaCertificateUsage, u8;
|
||||
|
||||
/// CA constraint
|
||||
(PKIX_TA => 0, "PKIX-TA")
|
||||
|
||||
/// Service certificate constraint
|
||||
(PKIX_EE => 1, "PKIX-EE")
|
||||
|
||||
/// Trust anchor assertion
|
||||
(DANE_TA => 2, "DANE-TA")
|
||||
|
||||
/// Domain-issued certificate
|
||||
(DANE_EE => 3, "DANE-EE")
|
||||
|
||||
/// Reserved for Private Use
|
||||
(PRIVCERT => 255, "PrivCert")
|
||||
}
|
||||
|
||||
int_enum_str_decimal!(TlsaCertificateUsage, u8);
|
||||
int_enum_zonefile_fmt_decimal!(
|
||||
TlsaCertificateUsage,
|
||||
"certificate usage type"
|
||||
);
|
||||
|
||||
//------------ TlsaSelector --------------------------------------------------
|
||||
|
||||
int_enum! {
|
||||
/// TLSA Selector type.
|
||||
///
|
||||
/// This type specifies which part of the TLS certificate presented by the server will be
|
||||
/// matched against the association data
|
||||
///
|
||||
/// For the currently registered values see the [IANA registration]. This
|
||||
/// type is complete as of 2025-09-04.
|
||||
///
|
||||
/// [TLSA]: ../../../rdata/tlsa/index.html
|
||||
/// [IANA registration]: https://www.iana.org/assignments/dane-parameters/dane-parameters.xhtml#selectors
|
||||
=>
|
||||
TlsaSelector, u8;
|
||||
|
||||
/// Full certificate
|
||||
(CERT => 0, "Cert")
|
||||
|
||||
/// SubjectPublicKeyInfo
|
||||
(SPKI => 1, "SPKI")
|
||||
|
||||
/// Reserved for Private Use
|
||||
(PRIVSEL => 255, "PrivSel")
|
||||
}
|
||||
|
||||
int_enum_str_decimal!(TlsaSelector, u8);
|
||||
int_enum_zonefile_fmt_decimal!(TlsaSelector, "selector");
|
||||
|
||||
//------------ TlsaMatchingType ----------------------------------------------
|
||||
|
||||
int_enum! {
|
||||
/// TLSA Matching Type type.
|
||||
///
|
||||
/// This type specifies how the certificate association is presented.
|
||||
///
|
||||
/// For the currently registered values see the [IANA registration]. This
|
||||
/// type is complete as of 2025-09-04.
|
||||
///
|
||||
/// [TLSA]: ../../../rdata/tlsa/index.html
|
||||
/// [IANA registration]: https://www.iana.org/assignments/dane-parameters/dane-parameters.xhtml#matching-types
|
||||
=>
|
||||
TlsaMatchingType, u8;
|
||||
|
||||
/// No hash used
|
||||
(FULL => 0, "Full")
|
||||
|
||||
/// 256 bit hash by SHA2
|
||||
(SHA2_256 => 1, "SHA2-256")
|
||||
|
||||
/// 512 bit hash by SHA2
|
||||
(SHA2_512 => 2, "SHA2-512")
|
||||
|
||||
/// Reserved for Private Use
|
||||
(PRIVMATCH => 255, "PrivMatch")
|
||||
}
|
||||
|
||||
int_enum_str_decimal!(TlsaMatchingType, u8);
|
||||
int_enum_zonefile_fmt_decimal!(TlsaMatchingType, "matching type");
|
||||
@@ -211,6 +211,18 @@ pub trait Scanner {
|
||||
/// It can be of any length.
|
||||
fn scan_octets(&mut self) -> Result<Self::Octets, Self::Error>;
|
||||
|
||||
/// Scans a token into an octets sequence combining tokens that are not
|
||||
/// separated by whitespace into a single token (used for SVCB quoted
|
||||
/// SvcParamValues).
|
||||
///
|
||||
/// The returned sequence has all symbols converted into their octets.
|
||||
/// It can be of any length.
|
||||
fn scan_svcb_octets(&mut self) -> Result<Self::Octets, Self::Error> {
|
||||
Err(Self::Error::custom(
|
||||
"Scanning SVCB octets is only implemented by some Scanners",
|
||||
))
|
||||
}
|
||||
|
||||
/// Scans a token as a borrowed ASCII string.
|
||||
///
|
||||
/// If the next token contains non-ascii characters, returns an error.
|
||||
|
||||
@@ -0,0 +1,922 @@
|
||||
//! IPSECKEY record data.
|
||||
//!
|
||||
//! The IPSECKEY Resource Record is used to publish a public key that is to be
|
||||
//! associated with a domain name for use with the IPsec protocol suite.
|
||||
//!
|
||||
//! [RFC 4025]: https://tools.ietf.org/html/rfc4025
|
||||
|
||||
// Currently a false positive on Ipseckey. We cannot apply it there because
|
||||
// the allow attribute doesn't get copied to the code generated by serde.
|
||||
#![allow(clippy::needless_maybe_sized)]
|
||||
|
||||
use crate::base::cmp::CanonicalOrd;
|
||||
use crate::base::iana::{IpseckeyAlgorithm, IpseckeyGatewayType, Rtype};
|
||||
use crate::base::name::FlattenInto;
|
||||
use crate::base::rdata::{ComposeRecordData, RecordData};
|
||||
use crate::base::scan::{Scan, Scanner, ScannerError};
|
||||
use crate::base::wire::{Composer, FormError, ParseError};
|
||||
use crate::base::zonefile_fmt::{self, Formatter, ZonefileFmt};
|
||||
use crate::base::{ParsedName, ToName};
|
||||
use crate::utils::base64;
|
||||
use core::cmp::Ordering;
|
||||
use core::{fmt, hash};
|
||||
use octseq::octets::{Octets, OctetsFrom, OctetsInto};
|
||||
use octseq::parse::Parser;
|
||||
|
||||
use super::{Aaaa, A};
|
||||
|
||||
/// The IPSECKEY Resource Record is used to publish a public key that is to be
|
||||
/// associated with a domain name for use with the IPsec protocol suite.
|
||||
#[derive(Clone)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
serde(bound(
|
||||
serialize = "
|
||||
N: serde::Serialize,
|
||||
Octs: octseq::serde::SerializeOctets
|
||||
",
|
||||
deserialize = "
|
||||
N: serde::Deserialize<'de>,
|
||||
Octs: octseq::serde::DeserializeOctets<'de>
|
||||
",
|
||||
))
|
||||
)]
|
||||
pub struct Ipseckey<Octs: ?Sized, N> {
|
||||
precedence: u8,
|
||||
gateway_type: IpseckeyGatewayType,
|
||||
algorithm: IpseckeyAlgorithm,
|
||||
|
||||
/// "The gateway to which an IPsec tunnel may be created"
|
||||
///
|
||||
/// There are three formats:
|
||||
///
|
||||
/// - IPv4 address: This is a 32-bit number in network byte order.
|
||||
/// - IPv6 address: This is a 128-bit number in network byte order.
|
||||
/// - A normal wire-encoded domain name, always uncompressed.
|
||||
/// - The domain MUST equal '.' if the gateway type is
|
||||
/// IpseckeyGatewayType::NONE
|
||||
gateway: IpseckeyGateway<N>,
|
||||
|
||||
// May be zero bytes long
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
serde(
|
||||
serialize_with = "octseq::serde::SerializeOctets::serialize_octets",
|
||||
deserialize_with = "octseq::serde::DeserializeOctets::deserialize_octets",
|
||||
)
|
||||
)]
|
||||
key: Octs,
|
||||
}
|
||||
|
||||
//------------ Ipseckey ------------------------------------------------------
|
||||
|
||||
impl Ipseckey<(), ()> {
|
||||
/// The rtype of this record data type.
|
||||
pub(crate) const RTYPE: Rtype = Rtype::IPSECKEY;
|
||||
}
|
||||
|
||||
impl<Octs, N> Ipseckey<Octs, N> {
|
||||
/// Create a Ipseckey record data from provided parameters.
|
||||
pub fn new(
|
||||
precedence: u8,
|
||||
algorithm: IpseckeyAlgorithm,
|
||||
gateway: IpseckeyGateway<N>,
|
||||
key: Octs,
|
||||
) -> Self {
|
||||
let gateway_type = match gateway {
|
||||
IpseckeyGateway::None => IpseckeyGatewayType::NONE,
|
||||
IpseckeyGateway::Ipv4(_) => IpseckeyGatewayType::IPV4,
|
||||
IpseckeyGateway::Ipv6(_) => IpseckeyGatewayType::IPV6,
|
||||
IpseckeyGateway::Name(_) => IpseckeyGatewayType::NAME,
|
||||
};
|
||||
Self {
|
||||
precedence,
|
||||
gateway_type,
|
||||
algorithm,
|
||||
gateway,
|
||||
key,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the precedence field.
|
||||
pub fn precedence(&self) -> u8 {
|
||||
self.precedence
|
||||
}
|
||||
|
||||
/// Get the gateway type field.
|
||||
pub fn gateway_type(&self) -> IpseckeyGatewayType {
|
||||
self.gateway_type
|
||||
}
|
||||
|
||||
/// Get the public key algorithm field.
|
||||
pub fn algorithm(&self) -> IpseckeyAlgorithm {
|
||||
self.algorithm
|
||||
}
|
||||
|
||||
/// Get the gateway field.
|
||||
pub fn gateway(&self) -> &IpseckeyGateway<N> {
|
||||
&self.gateway
|
||||
}
|
||||
|
||||
/// Get the public key field.
|
||||
pub fn key(&self) -> &Octs {
|
||||
&self.key
|
||||
}
|
||||
|
||||
/// Parse the record data from zonefile format.
|
||||
pub fn scan<S: Scanner<Octets = Octs, Name = N>>(
|
||||
scanner: &mut S,
|
||||
) -> Result<Self, S::Error>
|
||||
where
|
||||
Octs: AsRef<[u8]>,
|
||||
{
|
||||
let precedence = u8::scan(scanner)?;
|
||||
// Using u8::scan instead of Ipseckey{GatewayType,Algorithm}::scan to
|
||||
// restrict the allowed input to integers and disallow mnemonics.
|
||||
let gateway_type = u8::scan(scanner)?.into();
|
||||
let algorithm = u8::scan(scanner)?.into();
|
||||
let gateway = IpseckeyGateway::scan(scanner, gateway_type)?;
|
||||
let key = scanner.convert_entry(base64::SymbolConverter::new())?;
|
||||
if key.as_ref().is_empty() && algorithm != IpseckeyAlgorithm::NONE {
|
||||
return Err(ScannerError::custom("Missing IPSECKEY public key field. The public key field may only be omitted when the algorithm is specified as 0"));
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
precedence,
|
||||
gateway_type,
|
||||
algorithm,
|
||||
gateway,
|
||||
key,
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn flatten<TargetOcts, TargetName>(
|
||||
self,
|
||||
) -> Result<Ipseckey<TargetOcts, TargetName>, N::AppendError>
|
||||
where
|
||||
TargetOcts: OctetsFrom<Octs>,
|
||||
N: FlattenInto<TargetName, AppendError = TargetOcts::Error>,
|
||||
{
|
||||
let Ipseckey {
|
||||
precedence,
|
||||
gateway_type,
|
||||
algorithm,
|
||||
gateway,
|
||||
key,
|
||||
} = self;
|
||||
|
||||
Ok(Ipseckey {
|
||||
precedence,
|
||||
gateway_type,
|
||||
algorithm,
|
||||
gateway: gateway.flatten()?,
|
||||
key: key.try_octets_into()?,
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn convert_octets<TargetOcts, TargetName>(
|
||||
self,
|
||||
) -> Result<Ipseckey<TargetOcts, TargetName>, TargetOcts::Error>
|
||||
where
|
||||
TargetOcts: OctetsFrom<Octs>,
|
||||
TargetName: OctetsFrom<N, Error = TargetOcts::Error>,
|
||||
{
|
||||
let Ipseckey {
|
||||
precedence,
|
||||
gateway_type,
|
||||
algorithm,
|
||||
gateway,
|
||||
key,
|
||||
} = self;
|
||||
|
||||
Ok(Ipseckey {
|
||||
precedence,
|
||||
gateway_type,
|
||||
algorithm,
|
||||
gateway: gateway.convert_octets()?,
|
||||
key: key.try_octets_into()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<Octs> Ipseckey<Octs, ParsedName<Octs>> {
|
||||
/// Parse the record data from wire format.
|
||||
pub fn parse<'a, Src: Octets<Range<'a> = Octs> + ?Sized>(
|
||||
parser: &mut Parser<'a, Src>,
|
||||
) -> Result<Self, ParseError> {
|
||||
let precedence = parser.parse_u8()?;
|
||||
let gateway_type = IpseckeyGatewayType::parse(parser)?;
|
||||
let algorithm = IpseckeyAlgorithm::parse(parser)?;
|
||||
let gateway = IpseckeyGateway::parse(parser, gateway_type)?;
|
||||
let len_key = parser.remaining();
|
||||
if len_key == 0 && algorithm != IpseckeyAlgorithm::NONE {
|
||||
return Err(ParseError::ShortInput);
|
||||
}
|
||||
let key = parser.parse_octets(len_key)?;
|
||||
Ok(Self {
|
||||
precedence,
|
||||
gateway_type,
|
||||
algorithm,
|
||||
gateway,
|
||||
key,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<Octs, N> RecordData for Ipseckey<Octs, N> {
|
||||
fn rtype(&self) -> Rtype {
|
||||
Ipseckey::RTYPE
|
||||
}
|
||||
}
|
||||
|
||||
impl<Octs: AsRef<[u8]>, N: ToName> ComposeRecordData for Ipseckey<Octs, N> {
|
||||
fn rdlen(&self, _compress: bool) -> Option<u16> {
|
||||
Some(
|
||||
// precedence=1 + gateway_type=1 + algorithm=1 + gateway + key
|
||||
u16::try_from(
|
||||
1 + 1
|
||||
+ 1
|
||||
+ self.gateway.rdlen() as usize
|
||||
+ self.key.as_ref().len(),
|
||||
)
|
||||
.expect("long IPSECKEY rdata"),
|
||||
)
|
||||
}
|
||||
|
||||
fn compose_rdata<Target: Composer + ?Sized>(
|
||||
&self,
|
||||
target: &mut Target,
|
||||
) -> Result<(), Target::AppendError> {
|
||||
target.append_slice(&[self.precedence])?;
|
||||
target.append_slice(&[self.gateway_type.into()])?;
|
||||
target.append_slice(&[self.algorithm.into()])?;
|
||||
self.gateway.compose_rdata(target)?;
|
||||
target.append_slice(self.key.as_ref())
|
||||
}
|
||||
|
||||
fn compose_canonical_rdata<Target: Composer + ?Sized>(
|
||||
&self,
|
||||
target: &mut Target,
|
||||
) -> Result<(), Target::AppendError> {
|
||||
self.compose_rdata(target)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Octs: AsRef<[u8]>, N: hash::Hash> hash::Hash for Ipseckey<Octs, N> {
|
||||
fn hash<H: hash::Hasher>(&self, state: &mut H) {
|
||||
self.precedence.hash(state);
|
||||
self.gateway_type.hash(state);
|
||||
self.algorithm.hash(state);
|
||||
self.gateway.hash(state);
|
||||
self.key.as_ref().hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl<Octs, N, OtherOcts, OtherName> PartialEq<Ipseckey<OtherOcts, OtherName>>
|
||||
for Ipseckey<Octs, N>
|
||||
where
|
||||
Octs: AsRef<[u8]> + ?Sized,
|
||||
OtherOcts: AsRef<[u8]> + ?Sized,
|
||||
N: ToName,
|
||||
OtherName: ToName,
|
||||
{
|
||||
fn eq(&self, other: &Ipseckey<OtherOcts, OtherName>) -> bool {
|
||||
self.precedence.eq(&other.precedence)
|
||||
&& self.gateway_type.eq(&other.gateway_type)
|
||||
&& self.algorithm.eq(&other.algorithm)
|
||||
&& self.gateway.eq(&other.gateway)
|
||||
&& self.key.as_ref().eq(other.key.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl<Octs: AsRef<[u8]> + ?Sized, N: ToName> Eq for Ipseckey<Octs, N> {}
|
||||
|
||||
impl<Octs: AsRef<[u8]>, N: fmt::Display> fmt::Display for Ipseckey<Octs, N> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{} {} {} {} ( ",
|
||||
self.precedence,
|
||||
u8::from(self.gateway_type),
|
||||
u8::from(self.algorithm),
|
||||
self.gateway,
|
||||
)?;
|
||||
base64::display(&self.key, f)?;
|
||||
write!(f, " )")
|
||||
}
|
||||
}
|
||||
|
||||
impl<Octs: AsRef<[u8]>, N: fmt::Debug> fmt::Debug for Ipseckey<Octs, N> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
// f.write_str("Ipseckey(")?;
|
||||
// fmt::Display::fmt(self, f)?;
|
||||
// f.write_str(")")
|
||||
f.debug_struct("Ipseckey")
|
||||
.field("precedence", &self.precedence)
|
||||
.field("gateway_type", &self.gateway_type)
|
||||
.field("algorithm", &self.algorithm)
|
||||
.field("gateway", &self.gateway)
|
||||
.field(
|
||||
"key",
|
||||
&format_args!("{}", base64::encode_display(&self.key)),
|
||||
)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<Octs: AsRef<[u8]>, N: ToName> ZonefileFmt for Ipseckey<Octs, N> {
|
||||
fn fmt(&self, p: &mut impl Formatter) -> zonefile_fmt::Result {
|
||||
p.block(|p| {
|
||||
p.write_token(self.precedence)?;
|
||||
p.write_comment("precedence")?;
|
||||
p.write_show(self.gateway_type)?;
|
||||
p.write_comment("gateway type")?;
|
||||
p.write_show(self.algorithm)?;
|
||||
p.write_comment("algorithm")?;
|
||||
p.write_show(&self.gateway)?;
|
||||
p.write_comment("gateway")?;
|
||||
p.write_token(base64::encode_display(&self.key))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<Octs, OtherOcts, N, OtherName> PartialOrd<Ipseckey<OtherOcts, OtherName>>
|
||||
for Ipseckey<Octs, N>
|
||||
where
|
||||
Octs: AsRef<[u8]>,
|
||||
OtherOcts: AsRef<[u8]>,
|
||||
N: ToName,
|
||||
OtherName: ToName,
|
||||
{
|
||||
fn partial_cmp(
|
||||
&self,
|
||||
other: &Ipseckey<OtherOcts, OtherName>,
|
||||
) -> Option<Ordering> {
|
||||
match self.precedence.partial_cmp(&other.precedence) {
|
||||
Some(Ordering::Equal) => {}
|
||||
other => return other,
|
||||
}
|
||||
match self.gateway_type.partial_cmp(&other.gateway_type) {
|
||||
Some(Ordering::Equal) => {}
|
||||
other => return other,
|
||||
}
|
||||
match self.algorithm.partial_cmp(&other.algorithm) {
|
||||
Some(Ordering::Equal) => {}
|
||||
other => return other,
|
||||
}
|
||||
match self.gateway.partial_cmp(&other.gateway) {
|
||||
Some(Ordering::Equal) => {}
|
||||
other => return other,
|
||||
}
|
||||
self.key.as_ref().partial_cmp(other.key.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl<Octs, OtherOcts, N, OtherName>
|
||||
CanonicalOrd<Ipseckey<OtherOcts, OtherName>> for Ipseckey<Octs, N>
|
||||
where
|
||||
Octs: AsRef<[u8]>,
|
||||
OtherOcts: AsRef<[u8]>,
|
||||
N: ToName,
|
||||
OtherName: ToName,
|
||||
{
|
||||
fn canonical_cmp(
|
||||
&self,
|
||||
other: &Ipseckey<OtherOcts, OtherName>,
|
||||
) -> Ordering {
|
||||
match self.precedence.cmp(&other.precedence) {
|
||||
Ordering::Equal => {}
|
||||
other => return other,
|
||||
}
|
||||
match self.gateway_type.cmp(&other.gateway_type) {
|
||||
Ordering::Equal => {}
|
||||
other => return other,
|
||||
}
|
||||
match self.algorithm.cmp(&other.algorithm) {
|
||||
Ordering::Equal => {}
|
||||
other => return other,
|
||||
}
|
||||
match self.gateway.partial_cmp(&other.gateway) {
|
||||
Some(Ordering::Equal) => {}
|
||||
Some(other) => return other,
|
||||
None => unreachable!("The gateway will be the same variant and therefore have an ordering, because the gateway_type above was Equal"),
|
||||
}
|
||||
self.key.as_ref().cmp(other.key.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl<Octs: AsRef<[u8]>, N: ToName> Ord for Ipseckey<Octs, N> {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
match self.precedence.cmp(&other.precedence) {
|
||||
Ordering::Equal => {}
|
||||
other => return other,
|
||||
}
|
||||
match self.gateway_type.cmp(&other.gateway_type) {
|
||||
Ordering::Equal => {}
|
||||
other => return other,
|
||||
}
|
||||
match self.algorithm.cmp(&other.algorithm) {
|
||||
Ordering::Equal => {}
|
||||
other => return other,
|
||||
}
|
||||
match self.gateway.partial_cmp(&other.gateway) {
|
||||
Some(Ordering::Equal) => {}
|
||||
Some(other) => return other,
|
||||
None => unreachable!("The gateway will be the same variant and therefore have an ordering, because the gateway_type above was Equal"),
|
||||
}
|
||||
self.key.as_ref().cmp(other.key.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
//------------ IpseckeyGateway -----------------------------------------------
|
||||
|
||||
#[derive(Clone)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum IpseckeyGateway<N> {
|
||||
None,
|
||||
Ipv4(A),
|
||||
Ipv6(Aaaa),
|
||||
Name(N),
|
||||
}
|
||||
|
||||
impl<N> IpseckeyGateway<N> {
|
||||
pub fn rdlen(&self) -> u16
|
||||
where
|
||||
N: ToName,
|
||||
{
|
||||
match self {
|
||||
IpseckeyGateway::None => 0,
|
||||
IpseckeyGateway::Ipv4(_) => 4,
|
||||
IpseckeyGateway::Ipv6(_) => 16,
|
||||
IpseckeyGateway::Name(n) => n.compose_len(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_correct_gateway_type(&self, gwt: IpseckeyGatewayType) -> bool {
|
||||
matches!(
|
||||
(self, gwt),
|
||||
(IpseckeyGateway::None, IpseckeyGatewayType::NONE)
|
||||
| (IpseckeyGateway::Ipv4(_), IpseckeyGatewayType::IPV4)
|
||||
| (IpseckeyGateway::Ipv6(_), IpseckeyGatewayType::IPV6)
|
||||
| (IpseckeyGateway::Name(_), IpseckeyGatewayType::NAME)
|
||||
)
|
||||
}
|
||||
|
||||
pub fn scan<S: Scanner<Name = N>>(
|
||||
scanner: &mut S,
|
||||
gateway_type: IpseckeyGatewayType,
|
||||
) -> Result<Self, S::Error> {
|
||||
Ok(match gateway_type {
|
||||
IpseckeyGatewayType::NONE => {
|
||||
scanner.scan_ascii_str(|s| {
|
||||
if s == "." {
|
||||
Ok(Self::None)
|
||||
} else {
|
||||
Err(ScannerError::custom("Invalid IPSECKEY gateway. As the gateway type is specified as 0 (None), the gateway MUST be set to '.'"))
|
||||
}
|
||||
})?
|
||||
},
|
||||
IpseckeyGatewayType::IPV4 => Self::Ipv4(A::scan(scanner)?),
|
||||
IpseckeyGatewayType::IPV6 => Self::Ipv6(Aaaa::scan(scanner)?),
|
||||
IpseckeyGatewayType::NAME => Self::Name(scanner.scan_name()?),
|
||||
_ => {
|
||||
return Err(ScannerError::custom(
|
||||
"Unknown IPSECKEY gateway type",
|
||||
))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn flatten<TargetName>(
|
||||
self,
|
||||
) -> Result<IpseckeyGateway<TargetName>, N::AppendError>
|
||||
where
|
||||
N: FlattenInto<TargetName>,
|
||||
{
|
||||
Ok(match self {
|
||||
IpseckeyGateway::None => IpseckeyGateway::None,
|
||||
IpseckeyGateway::Ipv4(a) => IpseckeyGateway::Ipv4(a),
|
||||
IpseckeyGateway::Ipv6(aaaa) => IpseckeyGateway::Ipv6(aaaa),
|
||||
IpseckeyGateway::Name(n) => {
|
||||
IpseckeyGateway::Name(n.try_flatten_into()?)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn convert_octets<Target: OctetsFrom<N>>(
|
||||
self,
|
||||
) -> Result<IpseckeyGateway<Target>, Target::Error> {
|
||||
Ok(match self {
|
||||
IpseckeyGateway::None => IpseckeyGateway::None,
|
||||
IpseckeyGateway::Ipv4(a) => IpseckeyGateway::Ipv4(a),
|
||||
IpseckeyGateway::Ipv6(aaaa) => IpseckeyGateway::Ipv6(aaaa),
|
||||
IpseckeyGateway::Name(n) => {
|
||||
IpseckeyGateway::Name(n.try_octets_into()?)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn compose_rdata<Target: Composer + ?Sized>(
|
||||
&self,
|
||||
target: &mut Target,
|
||||
) -> Result<(), Target::AppendError>
|
||||
where
|
||||
N: ToName,
|
||||
{
|
||||
match self {
|
||||
IpseckeyGateway::None => (),
|
||||
IpseckeyGateway::Ipv4(a) => a.compose_rdata(target)?,
|
||||
IpseckeyGateway::Ipv6(aaaa) => aaaa.compose_rdata(target)?,
|
||||
IpseckeyGateway::Name(n) => n.compose(target)?,
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<N: hash::Hash> hash::Hash for IpseckeyGateway<N> {
|
||||
fn hash<H: hash::Hasher>(&self, state: &mut H) {
|
||||
match self {
|
||||
IpseckeyGateway::None => todo!(),
|
||||
IpseckeyGateway::Ipv4(a) => a.hash(state),
|
||||
IpseckeyGateway::Ipv6(aaaa) => aaaa.hash(state),
|
||||
IpseckeyGateway::Name(n) => n.hash(state),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<N, OtherName> PartialEq<IpseckeyGateway<OtherName>>
|
||||
for IpseckeyGateway<N>
|
||||
where
|
||||
N: ToName,
|
||||
OtherName: ToName,
|
||||
{
|
||||
fn eq(&self, other: &IpseckeyGateway<OtherName>) -> bool {
|
||||
match (self, other) {
|
||||
(IpseckeyGateway::None, IpseckeyGateway::None) => true,
|
||||
(IpseckeyGateway::Ipv4(a), IpseckeyGateway::Ipv4(o)) => a.eq(o),
|
||||
(IpseckeyGateway::Ipv6(aaaa), IpseckeyGateway::Ipv6(o)) => {
|
||||
aaaa.eq(o)
|
||||
}
|
||||
(IpseckeyGateway::Name(n), IpseckeyGateway::Name(o)) => {
|
||||
n.name_eq(o)
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<N, OtherName> PartialOrd<IpseckeyGateway<OtherName>>
|
||||
for IpseckeyGateway<N>
|
||||
where
|
||||
N: ToName,
|
||||
OtherName: ToName,
|
||||
{
|
||||
fn partial_cmp(
|
||||
&self,
|
||||
other: &IpseckeyGateway<OtherName>,
|
||||
) -> Option<Ordering> {
|
||||
match (self, other) {
|
||||
(IpseckeyGateway::None, IpseckeyGateway::None) => {
|
||||
Some(Ordering::Equal)
|
||||
}
|
||||
(IpseckeyGateway::Ipv4(a), IpseckeyGateway::Ipv4(o)) => {
|
||||
a.partial_cmp(o)
|
||||
}
|
||||
(IpseckeyGateway::Ipv6(aaaa), IpseckeyGateway::Ipv6(o)) => {
|
||||
aaaa.partial_cmp(o)
|
||||
}
|
||||
(IpseckeyGateway::Name(n), IpseckeyGateway::Name(o)) => {
|
||||
Some(n.name_cmp(o))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Octs> IpseckeyGateway<ParsedName<Octs>> {
|
||||
pub fn parse<'a, Src: Octets<Range<'a> = Octs> + ?Sized>(
|
||||
parser: &mut Parser<'a, Src>,
|
||||
gateway_type: IpseckeyGatewayType,
|
||||
) -> Result<Self, ParseError> {
|
||||
let len_gateway = match gateway_type {
|
||||
IpseckeyGatewayType::NONE => Some(0),
|
||||
IpseckeyGatewayType::IPV4 => Some(4),
|
||||
IpseckeyGatewayType::IPV6 => Some(16),
|
||||
IpseckeyGatewayType::NAME => None,
|
||||
_ => {
|
||||
return Err(ParseError::Form(FormError::new(
|
||||
"Unknown IPSECKEY gateway type",
|
||||
)))
|
||||
}
|
||||
};
|
||||
let remaining = parser.remaining();
|
||||
let gateway = if let Some(len_gateway) = len_gateway {
|
||||
if remaining < len_gateway {
|
||||
return Err(ParseError::ShortInput);
|
||||
}
|
||||
match gateway_type {
|
||||
IpseckeyGatewayType::NONE => IpseckeyGateway::None,
|
||||
IpseckeyGatewayType::IPV4 => {
|
||||
IpseckeyGateway::Ipv4(A::parse(parser)?)
|
||||
}
|
||||
IpseckeyGatewayType::IPV6 => {
|
||||
IpseckeyGateway::Ipv6(Aaaa::parse(parser)?)
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
} else {
|
||||
// Minimal length unknown, it contains a domain name
|
||||
let name = ParsedName::parse(parser)?;
|
||||
if name.is_compressed() {
|
||||
return Err(ParseError::Form(FormError::new(
|
||||
"IPSECKEY gateway contains compressed name",
|
||||
)));
|
||||
}
|
||||
IpseckeyGateway::Name(name)
|
||||
};
|
||||
Ok(gateway)
|
||||
}
|
||||
}
|
||||
|
||||
impl<N: ToName> ZonefileFmt for IpseckeyGateway<N> {
|
||||
fn fmt(&self, p: &mut impl Formatter) -> zonefile_fmt::Result {
|
||||
match self {
|
||||
IpseckeyGateway::None => (),
|
||||
IpseckeyGateway::Ipv4(a) => p.write_show(a)?,
|
||||
IpseckeyGateway::Ipv6(aaaa) => p.write_show(aaaa)?,
|
||||
IpseckeyGateway::Name(n) => p.write_token(n.fmt_with_dot())?,
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<N: fmt::Display> fmt::Display for IpseckeyGateway<N> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
IpseckeyGateway::None => write!(f, "."),
|
||||
IpseckeyGateway::Ipv4(a) => write!(f, "{a}"),
|
||||
IpseckeyGateway::Ipv6(aaaa) => write!(f, "{aaaa}"),
|
||||
IpseckeyGateway::Name(n) => write!(f, "{n}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<N: fmt::Debug> fmt::Debug for IpseckeyGateway<N> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
IpseckeyGateway::None => write!(f, "IpseckeyGateway::None"),
|
||||
IpseckeyGateway::Ipv4(a) => {
|
||||
write!(f, "IpseckeyGateway::Ipv4({a:?})")
|
||||
}
|
||||
IpseckeyGateway::Ipv6(aaaa) => {
|
||||
write!(f, "IpseckeyGateway::Ipv6({aaaa:?})")
|
||||
}
|
||||
IpseckeyGateway::Name(n) => {
|
||||
write!(f, "IpseckeyGateway::Name({n:?})")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(all(feature = "std", feature = "bytes"))]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::base::rdata::test::{
|
||||
test_compose_parse, test_rdlen, test_scan,
|
||||
};
|
||||
use crate::base::Name;
|
||||
use crate::utils::base64::decode;
|
||||
use core::str::FromStr;
|
||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
||||
use std::string::ToString;
|
||||
use std::vec::Vec;
|
||||
|
||||
#[test]
|
||||
// allow redundant_closure because of lifetime shenanigans
|
||||
// in test_compose_parse(...::parse), "FnOnce is not general enough"
|
||||
#[allow(clippy::redundant_closure)]
|
||||
fn ipseckey_compose_parse_scan() {
|
||||
// From https://www.rfc-editor.org/rfc/rfc4025.html#section-3.2
|
||||
// IPSECKEY ( 10 1 2 192.0.2.38 AQNRU3mG7TVTO2BkR47usntb102uFJtugbo6BSGvgqt4AQ== )
|
||||
// IPSECKEY ( 10 0 2 . AQNRU3mG7TVTO2BkR47usntb102uFJtugbo6BSGvgqt4AQ== )
|
||||
// IPSECKEY ( 10 1 2 192.0.2.3 AQNRU3mG7TVTO2BkR47usntb102uFJtugbo6BSGvgqt4AQ== )
|
||||
// IPSECKEY ( 10 3 2 mygateway.example.com. AQNRU3mG7TVTO2BkR47usntb102uFJtugbo6BSGvgqt4AQ== )
|
||||
// IPSECKEY ( 10 2 2 2001:0DB8:0:8002::2000:1 AQNRU3mG7TVTO2BkR47usntb102uFJtugbo6BSGvgqt4AQ== )
|
||||
|
||||
let key_str = "AQNRU3mG7TVTO2BkR47usntb102uFJtugbo6BSGvgqt4AQ==";
|
||||
let key: Vec<u8> = decode(key_str).unwrap();
|
||||
for (precedence, gateway_type, algorithm, gateway_str, gateway) in [
|
||||
(
|
||||
10,
|
||||
IpseckeyGatewayType::from_int(1),
|
||||
2.into(),
|
||||
"192.0.2.38",
|
||||
IpseckeyGateway::<Name<Vec<u8>>>::Ipv4(
|
||||
Ipv4Addr::new(192, 0, 2, 38).into(),
|
||||
),
|
||||
),
|
||||
(
|
||||
10,
|
||||
0.into(),
|
||||
2.into(),
|
||||
".",
|
||||
IpseckeyGateway::<Name<Vec<u8>>>::None,
|
||||
),
|
||||
(
|
||||
10,
|
||||
1.into(),
|
||||
2.into(),
|
||||
"192.0.2.3",
|
||||
IpseckeyGateway::<Name<Vec<u8>>>::Ipv4(
|
||||
Ipv4Addr::new(192, 0, 2, 3).into(),
|
||||
),
|
||||
),
|
||||
(
|
||||
10,
|
||||
3.into(),
|
||||
2.into(),
|
||||
"mygateway.example.com.",
|
||||
IpseckeyGateway::<Name<Vec<u8>>>::Name(
|
||||
Name::from_str("mygateway.example.com.").unwrap(),
|
||||
),
|
||||
),
|
||||
(
|
||||
10,
|
||||
2.into(),
|
||||
2.into(),
|
||||
"2001:0DB8:0:8002::2000:1",
|
||||
IpseckeyGateway::<Name<Vec<u8>>>::Ipv6(
|
||||
Ipv6Addr::new(
|
||||
0x2001, 0x0DB8, 0x0, 0x8002, 0x0, 0x0, 0x2000, 0x1,
|
||||
)
|
||||
.into(),
|
||||
),
|
||||
),
|
||||
] {
|
||||
let rdata = Ipseckey::new(precedence, algorithm, gateway, &key);
|
||||
test_rdlen(&rdata);
|
||||
test_compose_parse(&rdata, |parser| Ipseckey::parse(parser));
|
||||
test_scan(
|
||||
&[
|
||||
&precedence.to_string(),
|
||||
&u8::from(gateway_type).to_string(),
|
||||
&u8::from(algorithm).to_string(),
|
||||
gateway_str,
|
||||
key_str,
|
||||
],
|
||||
Ipseckey::scan,
|
||||
&rdata,
|
||||
);
|
||||
}
|
||||
|
||||
// IPSECKEY ( 10 0 0 . )
|
||||
let rdata = Ipseckey::new(
|
||||
10,
|
||||
0.into(),
|
||||
IpseckeyGateway::<Name<Vec<u8>>>::None,
|
||||
&[],
|
||||
);
|
||||
test_rdlen(&rdata);
|
||||
test_compose_parse(&rdata, |parser| Ipseckey::parse(parser));
|
||||
test_scan(
|
||||
&[&10.to_string(), &0.to_string(), &0.to_string(), "."],
|
||||
Ipseckey::scan,
|
||||
&rdata,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
// allow redundant_closure because of lifetime shenanigans
|
||||
// in test_compose_parse(...::parse), "FnOnce is not general enough"
|
||||
#[allow(clippy::redundant_closure)]
|
||||
fn ipseckey_scan_wrong_gateway() {
|
||||
// IPSECKEY ( 10 0 2 this.should.be.just.dot. AQNRU3mG7TVTO2BkR47usntb102uFJtugbo6BSGvgqt4AQ== )
|
||||
let precedence = 10;
|
||||
let gateway_type = IpseckeyGatewayType::from_int(0);
|
||||
let algorithm = 2.into();
|
||||
let wrong_gateway_str = "this.should.be.just.dot.";
|
||||
// let wrong_gateway: IpseckeyGateway<Name<Vec<u8>>> =
|
||||
// IpseckeyGateway::Name(Name::from_str(wrong_gateway_str).unwrap());
|
||||
let correct_gateway = IpseckeyGateway::<Name<Vec<u8>>>::None;
|
||||
let key_str = "AQNRU3mG7TVTO2BkR47usntb102uFJtugbo6BSGvgqt4AQ==";
|
||||
let key: Vec<u8> = decode(key_str).unwrap();
|
||||
let correct_rdata =
|
||||
Ipseckey::new(precedence, algorithm, correct_gateway, key);
|
||||
// This should panic in the unwrap within test_scan
|
||||
test_scan(
|
||||
&[
|
||||
&precedence.to_string(),
|
||||
&u8::from(gateway_type).to_string(),
|
||||
&u8::from(algorithm).to_string(),
|
||||
wrong_gateway_str,
|
||||
key_str,
|
||||
],
|
||||
Ipseckey::scan,
|
||||
&correct_rdata,
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "zonefile")]
|
||||
#[test]
|
||||
fn ipseckey_parse_zonefile() {
|
||||
use crate::rdata::ZoneRecordData;
|
||||
use crate::zonefile::inplace::{Entry, Zonefile};
|
||||
|
||||
// From https://www.rfc-editor.org/rfc/rfc4025.html#section-3.2
|
||||
let content = r#"
|
||||
arpa. 86400 IN SOA ns1 admin 2018031900 (
|
||||
1800 900 604800 86400 )
|
||||
86400 IN NS ns1
|
||||
86400 IN NS ns2
|
||||
ns1 3600 IN A 203.0.113.63
|
||||
ns2 3600 IN AAAA 2001:db8::63
|
||||
|
||||
38.2.0.192.in-addr.arpa. 7200 IN IPSECKEY ( 10 1 2 192.0.2.38
|
||||
AQNRU3mG7TVTO2BkR47usntb102uFJtugbo6BSGvgqt4AQ== )
|
||||
38.2.0.192.in-addr.arpa. 7200 IN IPSECKEY ( 10 0 2 .
|
||||
AQNRU3mG7TVTO2BkR47usntb102uFJtugbo6BSGvgqt4AQ== )
|
||||
|
||||
38.2.0.192.in-addr.arpa. 7200 IN IPSECKEY ( 10 1 2
|
||||
192.0.2.3
|
||||
AQNRU3mG7TVTO2BkR47usntb102uFJtugbo6BSGvgqt4AQ== )
|
||||
|
||||
38.1.0.192.in-addr.arpa. 7200 IN IPSECKEY ( 10 3 2
|
||||
mygateway.example.com.
|
||||
AQNRU3mG7TVTO2BkR47usntb102uFJtugbo6BSGvgqt4AQ== )
|
||||
|
||||
$ORIGIN 1.0.0.0.0.0.2.8.B.D.0.1.0.0.2.ip6.arpa.
|
||||
0.d.4.0.3.0.e.f.f.f.3.f.0.1.2.0 7200 IN IPSECKEY ( 10 2 2
|
||||
2001:0DB8:0:8002::2000:1
|
||||
AQNRU3mG7TVTO2BkR47usntb102uFJtugbo6BSGvgqt4AQ== )
|
||||
"#;
|
||||
|
||||
let mut zone = Zonefile::load(&mut content.as_bytes()).unwrap();
|
||||
zone.set_origin(Name::root());
|
||||
let key_str = "AQNRU3mG7TVTO2BkR47usntb102uFJtugbo6BSGvgqt4AQ==";
|
||||
let key: Vec<u8> = decode(key_str).unwrap();
|
||||
let expected_ipseckeys = [
|
||||
Ipseckey::new(
|
||||
10,
|
||||
2.into(),
|
||||
IpseckeyGateway::<Name<Vec<u8>>>::Ipv4(
|
||||
Ipv4Addr::new(192, 0, 2, 38).into(),
|
||||
),
|
||||
&key,
|
||||
),
|
||||
Ipseckey::new(
|
||||
10,
|
||||
2.into(),
|
||||
IpseckeyGateway::<Name<Vec<u8>>>::None,
|
||||
&key,
|
||||
),
|
||||
Ipseckey::new(
|
||||
10,
|
||||
2.into(),
|
||||
IpseckeyGateway::<Name<Vec<u8>>>::Ipv4(
|
||||
Ipv4Addr::new(192, 0, 2, 3).into(),
|
||||
),
|
||||
&key,
|
||||
),
|
||||
Ipseckey::new(
|
||||
10,
|
||||
2.into(),
|
||||
IpseckeyGateway::<Name<Vec<u8>>>::Name(
|
||||
Name::from_str("mygateway.example.com.").unwrap(),
|
||||
),
|
||||
&key,
|
||||
),
|
||||
Ipseckey::new(
|
||||
10,
|
||||
2.into(),
|
||||
IpseckeyGateway::<Name<Vec<u8>>>::Ipv6(
|
||||
Ipv6Addr::new(
|
||||
0x2001, 0x0DB8, 0x0, 0x8002, 0x0, 0x0, 0x2000, 0x1,
|
||||
)
|
||||
.into(),
|
||||
),
|
||||
&key,
|
||||
),
|
||||
];
|
||||
let mut expected_idx = 0;
|
||||
while let Some(entry) = zone.next_entry().unwrap() {
|
||||
match entry {
|
||||
Entry::Record(record) => {
|
||||
if record.rtype() != Rtype::IPSECKEY {
|
||||
continue;
|
||||
}
|
||||
match record.into_data() {
|
||||
ZoneRecordData::Ipseckey(rd) => {
|
||||
assert_eq!(expected_ipseckeys[expected_idx], rd);
|
||||
expected_idx += 1;
|
||||
}
|
||||
_ => panic!(),
|
||||
}
|
||||
}
|
||||
_ => panic!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+25
-1
@@ -49,11 +49,15 @@ pub mod aaaa;
|
||||
pub mod cds;
|
||||
pub mod dname;
|
||||
pub mod dnssec;
|
||||
pub mod ipseckey;
|
||||
pub mod naptr;
|
||||
pub mod nsec3;
|
||||
pub mod openpgpkey;
|
||||
pub mod rfc1035;
|
||||
pub mod srv;
|
||||
pub mod sshfp;
|
||||
pub mod svcb;
|
||||
pub mod tlsa;
|
||||
pub mod tsig;
|
||||
pub mod zonemd;
|
||||
|
||||
@@ -117,6 +121,11 @@ rdata_types! {
|
||||
Ds<O>,
|
||||
}
|
||||
}
|
||||
ipseckey::{
|
||||
zone {
|
||||
Ipseckey<O, N>,
|
||||
}
|
||||
}
|
||||
naptr::{
|
||||
zone {
|
||||
Naptr<O, N>,
|
||||
@@ -128,17 +137,32 @@ rdata_types! {
|
||||
Nsec3param<O>,
|
||||
}
|
||||
}
|
||||
openpgpkey::{
|
||||
zone {
|
||||
Openpgpkey<O>,
|
||||
}
|
||||
}
|
||||
srv::{
|
||||
zone {
|
||||
Srv<N>,
|
||||
}
|
||||
}
|
||||
sshfp::{
|
||||
zone {
|
||||
Sshfp<O>,
|
||||
}
|
||||
}
|
||||
svcb::{
|
||||
pseudo {
|
||||
zone {
|
||||
Svcb<O, N>,
|
||||
Https<O, N>,
|
||||
}
|
||||
}
|
||||
tlsa::{
|
||||
zone {
|
||||
Tlsa<O>,
|
||||
}
|
||||
}
|
||||
tsig::{
|
||||
pseudo {
|
||||
Tsig<O, N>,
|
||||
|
||||
@@ -0,0 +1,264 @@
|
||||
//! OPENPGPKEY record data.
|
||||
//!
|
||||
//! The OPENPGPKEY Resource Record carries a single OpenPGP Transferable Public Key.
|
||||
//!
|
||||
//! [RFC 7929]: https://tools.ietf.org/html/rfc7929
|
||||
|
||||
// Currently a false positive on Openpgpkey. We cannot apply it there because
|
||||
// the allow attribute doesn't get copied to the code generated by serde.
|
||||
#![allow(clippy::needless_maybe_sized)]
|
||||
|
||||
use crate::base::cmp::CanonicalOrd;
|
||||
use crate::base::iana::Rtype;
|
||||
use crate::base::rdata::{ComposeRecordData, RecordData};
|
||||
use crate::base::scan::Scanner;
|
||||
use crate::base::wire::{Composer, ParseError};
|
||||
use crate::base::zonefile_fmt::{self, Formatter, ZonefileFmt};
|
||||
use crate::utils::base64;
|
||||
use core::cmp::Ordering;
|
||||
use core::{fmt, hash};
|
||||
use octseq::octets::{Octets, OctetsFrom, OctetsInto};
|
||||
use octseq::parse::Parser;
|
||||
|
||||
/// The OPENPGPKEY Resource Record carries a single OpenPGP Transferable Public Key.
|
||||
#[derive(Clone)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Openpgpkey<Octs: ?Sized> {
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
serde(
|
||||
serialize_with = "octseq::serde::SerializeOctets::serialize_octets",
|
||||
deserialize_with = "octseq::serde::DeserializeOctets::deserialize_octets",
|
||||
bound(
|
||||
serialize = "Octs: octseq::serde::SerializeOctets",
|
||||
deserialize = "Octs: octseq::serde::DeserializeOctets<'de>",
|
||||
)
|
||||
)
|
||||
)]
|
||||
key: Octs,
|
||||
}
|
||||
|
||||
impl Openpgpkey<()> {
|
||||
/// The rtype of this record data type.
|
||||
pub(crate) const RTYPE: Rtype = Rtype::OPENPGPKEY;
|
||||
}
|
||||
|
||||
impl<Octs> Openpgpkey<Octs> {
|
||||
/// Create a Openpgpkey record data from provided parameters.
|
||||
pub fn new(key: Octs) -> Self {
|
||||
Self { key }
|
||||
}
|
||||
|
||||
/// Get the key field.
|
||||
pub fn key(&self) -> &Octs {
|
||||
&self.key
|
||||
}
|
||||
|
||||
/// Parse the record data from wire format.
|
||||
pub fn parse<'a, Src: Octets<Range<'a> = Octs> + ?Sized>(
|
||||
parser: &mut Parser<'a, Src>,
|
||||
) -> Result<Self, ParseError> {
|
||||
let len = parser.remaining();
|
||||
let key = parser.parse_octets(len)?;
|
||||
Ok(Self { key })
|
||||
}
|
||||
|
||||
/// Parse the record data from zonefile format.
|
||||
pub fn scan<S: Scanner<Octets = Octs>>(
|
||||
scanner: &mut S,
|
||||
) -> Result<Self, S::Error> {
|
||||
let key = scanner.convert_entry(base64::SymbolConverter::new())?;
|
||||
|
||||
Ok(Self { key })
|
||||
}
|
||||
|
||||
pub(super) fn flatten<Target: OctetsFrom<Octs>>(
|
||||
self,
|
||||
) -> Result<Openpgpkey<Target>, Target::Error> {
|
||||
self.convert_octets()
|
||||
}
|
||||
|
||||
pub(super) fn convert_octets<Target: OctetsFrom<Octs>>(
|
||||
self,
|
||||
) -> Result<Openpgpkey<Target>, Target::Error> {
|
||||
let Openpgpkey { key } = self;
|
||||
|
||||
Ok(Openpgpkey {
|
||||
key: key.try_octets_into()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<Octs> RecordData for Openpgpkey<Octs> {
|
||||
fn rtype(&self) -> Rtype {
|
||||
Openpgpkey::RTYPE
|
||||
}
|
||||
}
|
||||
|
||||
impl<Octs: AsRef<[u8]>> ComposeRecordData for Openpgpkey<Octs> {
|
||||
fn rdlen(&self, _compress: bool) -> Option<u16> {
|
||||
Some(
|
||||
// key_len
|
||||
u16::try_from(self.key.as_ref().len())
|
||||
.expect("long OPENPGPKEY rdata"),
|
||||
)
|
||||
}
|
||||
|
||||
fn compose_rdata<Target: Composer + ?Sized>(
|
||||
&self,
|
||||
target: &mut Target,
|
||||
) -> Result<(), Target::AppendError> {
|
||||
target.append_slice(self.key.as_ref())
|
||||
}
|
||||
|
||||
fn compose_canonical_rdata<Target: Composer + ?Sized>(
|
||||
&self,
|
||||
target: &mut Target,
|
||||
) -> Result<(), Target::AppendError> {
|
||||
self.compose_rdata(target)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Octs: AsRef<[u8]>> hash::Hash for Openpgpkey<Octs> {
|
||||
fn hash<H: hash::Hasher>(&self, state: &mut H) {
|
||||
self.key.as_ref().hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl<Octs, Other> PartialEq<Openpgpkey<Other>> for Openpgpkey<Octs>
|
||||
where
|
||||
Octs: AsRef<[u8]> + ?Sized,
|
||||
Other: AsRef<[u8]> + ?Sized,
|
||||
{
|
||||
fn eq(&self, other: &Openpgpkey<Other>) -> bool {
|
||||
self.key.as_ref().eq(other.key.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl<Octs: AsRef<[u8]> + ?Sized> Eq for Openpgpkey<Octs> {}
|
||||
|
||||
impl<Octs: AsRef<[u8]>> fmt::Display for Openpgpkey<Octs> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "( ",)?;
|
||||
base64::display(&self.key, f)?;
|
||||
write!(f, " )")
|
||||
}
|
||||
}
|
||||
|
||||
impl<Octs: AsRef<[u8]>> fmt::Debug for Openpgpkey<Octs> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("Openpgpkey(")?;
|
||||
fmt::Display::fmt(self, f)?;
|
||||
f.write_str(")")
|
||||
}
|
||||
}
|
||||
|
||||
impl<Octs: AsRef<[u8]>> ZonefileFmt for Openpgpkey<Octs> {
|
||||
fn fmt(&self, p: &mut impl Formatter) -> zonefile_fmt::Result {
|
||||
p.block(|p| p.write_token(base64::encode_display(&self.key)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<Octs, Other> PartialOrd<Openpgpkey<Other>> for Openpgpkey<Octs>
|
||||
where
|
||||
Octs: AsRef<[u8]>,
|
||||
Other: AsRef<[u8]>,
|
||||
{
|
||||
fn partial_cmp(&self, other: &Openpgpkey<Other>) -> Option<Ordering> {
|
||||
self.key.as_ref().partial_cmp(other.key.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl<Octs, Other> CanonicalOrd<Openpgpkey<Other>> for Openpgpkey<Octs>
|
||||
where
|
||||
Octs: AsRef<[u8]>,
|
||||
Other: AsRef<[u8]>,
|
||||
{
|
||||
fn canonical_cmp(&self, other: &Openpgpkey<Other>) -> Ordering {
|
||||
self.key.as_ref().cmp(other.key.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl<Octs: AsRef<[u8]>> Ord for Openpgpkey<Octs> {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
self.key.as_ref().cmp(other.key.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(all(feature = "std", feature = "bytes"))]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::base::rdata::test::{
|
||||
test_compose_parse, test_rdlen, test_scan,
|
||||
};
|
||||
use crate::utils::base64::decode;
|
||||
use std::vec::Vec;
|
||||
|
||||
#[test]
|
||||
// allow redundant_closure because because of lifetime shenanigans
|
||||
// in test_compose_parse(Openpgpkey::parse), "FnOnce is not general enough"
|
||||
#[allow(clippy::redundant_closure)]
|
||||
fn openpgpkey_compose_parse_scan() {
|
||||
let key_str = "mDMEaLmjchYJKwYBBAHaRw8BAQdAaO6PfPJsT8to5dksKP1JsCmR0DqOTmVYLOv7mFeQPC+0HVRlc3QgVXNlciA8dGVzdEBubG5ldGxhYnMubmw+iJYEExYKAD4WIQT/B5WhrMOftpJIwIkxXU3+oVEKegUCaLmjcgIbAwUJBaOagAULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRAxXU3+oVEKemSgAP97Zvz+PWEJC9vhlSN4gVRPR9VZYhzGwfpixgRI4sqKfwD9FxJhsj43vGEbOLdsWwf/lQvkajRov5FpofS1IFy/dgi4OARouaNyEgorBgEEAZdVAQUBAQdAUQr9riJNCFWRzQ6q70B/H/o+uwvL6nGJRhWSg1v7mRkDAQgHiH4EGBYKACYWIQT/B5WhrMOftpJIwIkxXU3+oVEKegUCaLmjcgIbDAUJBaOagAAKCRAxXU3+oVEKeuX3APkB5piWOSbOPLvtiElIVTHT6gWlu1wSpVVzZEmgtnOpiQD+Kk/IFjHpT0RbgsIvI3qhnXWwHvIw4JxHS1a/piLwkwM=";
|
||||
let key: Vec<u8> = decode(key_str).unwrap();
|
||||
let rdata = Openpgpkey::new(key);
|
||||
test_rdlen(&rdata);
|
||||
test_compose_parse(&rdata, |parser| Openpgpkey::parse(parser));
|
||||
test_scan(&[key_str], Openpgpkey::scan, &rdata);
|
||||
}
|
||||
|
||||
#[cfg(feature = "zonefile")]
|
||||
#[test]
|
||||
fn openpgpkey_parse_zonefile() {
|
||||
use crate::base::Name;
|
||||
use crate::rdata::ZoneRecordData;
|
||||
use crate::zonefile::inplace::{Entry, Zonefile};
|
||||
|
||||
// section A.1
|
||||
let content = r#"
|
||||
example. 86400 IN SOA ns1 admin 2018031900 (
|
||||
1800 900 604800 86400 )
|
||||
86400 IN NS ns1
|
||||
86400 IN NS ns2
|
||||
86400 IN OPENPGPKEY (
|
||||
mDMEaLmjchYJKwYBBAHaRw8BAQdAaO6P
|
||||
fPJsT8to5dksKP1JsCmR0DqOTmVYLOv7
|
||||
mFeQPC+0HVRlc3QgVXNlciA8dGVzdEBu
|
||||
bG5ldGxhYnMubmw+iJYEExYKAD4WIQT/
|
||||
B5WhrMOftpJIwIkxXU3+oVEKegUCaLmj
|
||||
cgIbAwUJBaOagAULCQgHAgYVCgkICwIE
|
||||
FgIDAQIeAQIXgAAKCRAxXU3+oVEKemSg
|
||||
AP97Zvz+PWEJC9vhlSN4gVRPR9VZYhzG
|
||||
wfpixgRI4sqKfwD9FxJhsj43vGEbOLds
|
||||
Wwf/lQvkajRov5FpofS1IFy/dgi4OARo
|
||||
uaNyEgorBgEEAZdVAQUBAQdAUQr9riJN
|
||||
CFWRzQ6q70B/H/o+uwvL6nGJRhWSg1v7
|
||||
mRkDAQgHiH4EGBYKACYWIQT/B5WhrMOf
|
||||
tpJIwIkxXU3+oVEKegUCaLmjcgIbDAUJ
|
||||
BaOagAAKCRAxXU3+oVEKeuX3APkB5piW
|
||||
OSbOPLvtiElIVTHT6gWlu1wSpVVzZEmg
|
||||
tnOpiQD+Kk/IFjHpT0RbgsIvI3qhnXWw
|
||||
HvIw4JxHS1a/piLwkwM= )
|
||||
ns1 3600 IN A 203.0.113.63
|
||||
ns2 3600 IN AAAA 2001:db8::63
|
||||
"#;
|
||||
|
||||
let mut zone = Zonefile::load(&mut content.as_bytes()).unwrap();
|
||||
zone.set_origin(Name::root());
|
||||
while let Some(entry) = zone.next_entry().unwrap() {
|
||||
match entry {
|
||||
Entry::Record(record) => {
|
||||
if record.rtype() != Rtype::OPENPGPKEY {
|
||||
continue;
|
||||
}
|
||||
match record.into_data() {
|
||||
ZoneRecordData::Openpgpkey(_) => {}
|
||||
_ => panic!(),
|
||||
}
|
||||
}
|
||||
_ => panic!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,352 @@
|
||||
//! Record data from [RFC 4255]: SSHFP records.
|
||||
//!
|
||||
//! This RFC defines the SSHFP record type and is updated by [RFC 6594],
|
||||
//! [RFC 7479], and [RFC 8709].
|
||||
//!
|
||||
//! [RFC 4255]: https://tools.ietf.org/html/rfc4255
|
||||
//! [RFC 6594]: https://tools.ietf.org/html/rfc6594
|
||||
//! [RFC 7479]: https://tools.ietf.org/html/rfc7479
|
||||
//! [RFC 8709]: https://tools.ietf.org/html/rfc8709
|
||||
|
||||
// Currently a false positive on Sshfp. We cannot apply it there because
|
||||
// the allow attribute doesn't get copied to the code generated by serde.
|
||||
#![allow(clippy::needless_maybe_sized)]
|
||||
|
||||
use crate::base::cmp::CanonicalOrd;
|
||||
use crate::base::iana::{SshfpAlgorithm, SshfpType};
|
||||
use crate::base::rdata::{ComposeRecordData, RecordData};
|
||||
use crate::base::scan::Scanner;
|
||||
use crate::base::wire::{Composer, ParseError};
|
||||
use crate::base::zonefile_fmt::{self, Formatter, ZonefileFmt};
|
||||
use crate::base::Rtype;
|
||||
use crate::utils::base16;
|
||||
use core::cmp::Ordering;
|
||||
use core::{fmt, hash};
|
||||
use octseq::octets::{Octets, OctetsFrom, OctetsInto};
|
||||
use octseq::parse::Parser;
|
||||
|
||||
//------------ Sshfp ---------------------------------------------------------
|
||||
|
||||
#[derive(Clone)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Sshfp<Octs: ?Sized> {
|
||||
algorithm: SshfpAlgorithm,
|
||||
fingerprint_type: SshfpType,
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
serde(
|
||||
serialize_with = "octseq::serde::SerializeOctets::serialize_octets",
|
||||
deserialize_with = "octseq::serde::DeserializeOctets::deserialize_octets",
|
||||
bound(
|
||||
serialize = "Octs: octseq::serde::SerializeOctets",
|
||||
deserialize = "Octs: octseq::serde::DeserializeOctets<'de>",
|
||||
)
|
||||
)
|
||||
)]
|
||||
fingerprint: Octs,
|
||||
}
|
||||
|
||||
impl Sshfp<()> {
|
||||
/// The rtype of this record data type.
|
||||
pub(crate) const RTYPE: Rtype = Rtype::SSHFP;
|
||||
}
|
||||
|
||||
impl<Octs> Sshfp<Octs> {
|
||||
pub fn new(
|
||||
algorithm: SshfpAlgorithm,
|
||||
fingerprint_type: SshfpType,
|
||||
fingerprint: Octs,
|
||||
) -> Self {
|
||||
Sshfp {
|
||||
algorithm,
|
||||
fingerprint_type,
|
||||
fingerprint,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the algorithm field.
|
||||
pub fn algorithm(&self) -> SshfpAlgorithm {
|
||||
self.algorithm
|
||||
}
|
||||
|
||||
/// Get the fingerprint type field.
|
||||
pub fn fingerprint_type(&self) -> SshfpType {
|
||||
self.fingerprint_type
|
||||
}
|
||||
|
||||
/// Get the fingerprint field.
|
||||
pub fn fingerprint(&self) -> &Octs {
|
||||
&self.fingerprint
|
||||
}
|
||||
|
||||
/// Parse the record data from wire format.
|
||||
pub fn parse<'a, Src: Octets<Range<'a> = Octs> + ?Sized>(
|
||||
parser: &mut Parser<'a, Src>,
|
||||
) -> Result<Self, ParseError> {
|
||||
let algorithm = SshfpAlgorithm::parse(parser)?;
|
||||
let fingerprint_type = SshfpType::parse(parser)?;
|
||||
let len = parser.remaining();
|
||||
let fingerprint = parser.parse_octets(len)?;
|
||||
Ok(Self {
|
||||
algorithm,
|
||||
fingerprint_type,
|
||||
fingerprint,
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse the record data from zonefile format.
|
||||
pub fn scan<S: Scanner<Octets = Octs>>(
|
||||
scanner: &mut S,
|
||||
) -> Result<Self, S::Error> {
|
||||
let algorithm = SshfpAlgorithm::scan(scanner)?;
|
||||
let fingerprint_type = SshfpType::scan(scanner)?;
|
||||
let fingerprint =
|
||||
scanner.convert_entry(base16::SymbolConverter::new())?;
|
||||
|
||||
Ok(Self {
|
||||
algorithm,
|
||||
fingerprint_type,
|
||||
fingerprint,
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn flatten<Target: OctetsFrom<Octs>>(
|
||||
self,
|
||||
) -> Result<Sshfp<Target>, Target::Error> {
|
||||
self.convert_octets()
|
||||
}
|
||||
|
||||
pub(super) fn convert_octets<Target: OctetsFrom<Octs>>(
|
||||
self,
|
||||
) -> Result<Sshfp<Target>, Target::Error> {
|
||||
let Sshfp {
|
||||
algorithm,
|
||||
fingerprint_type,
|
||||
fingerprint,
|
||||
} = self;
|
||||
|
||||
Ok(Sshfp {
|
||||
algorithm,
|
||||
fingerprint_type,
|
||||
fingerprint: fingerprint.try_octets_into()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<Octs> RecordData for Sshfp<Octs> {
|
||||
fn rtype(&self) -> Rtype {
|
||||
Sshfp::RTYPE
|
||||
}
|
||||
}
|
||||
|
||||
impl<Octs: AsRef<[u8]>> ComposeRecordData for Sshfp<Octs> {
|
||||
fn rdlen(&self, _compress: bool) -> Option<u16> {
|
||||
Some(
|
||||
// algorithm + fingerprint_type + fingerprint_len
|
||||
u16::try_from(1 + 1 + self.fingerprint.as_ref().len())
|
||||
.expect("long SSHFP rdata"),
|
||||
)
|
||||
}
|
||||
|
||||
fn compose_rdata<Target: Composer + ?Sized>(
|
||||
&self,
|
||||
target: &mut Target,
|
||||
) -> Result<(), Target::AppendError> {
|
||||
target.append_slice(&[self.algorithm.into()])?;
|
||||
target.append_slice(&[self.fingerprint_type.into()])?;
|
||||
target.append_slice(self.fingerprint.as_ref())
|
||||
}
|
||||
|
||||
fn compose_canonical_rdata<Target: Composer + ?Sized>(
|
||||
&self,
|
||||
target: &mut Target,
|
||||
) -> Result<(), Target::AppendError> {
|
||||
self.compose_rdata(target)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Octs: AsRef<[u8]>> hash::Hash for Sshfp<Octs> {
|
||||
fn hash<H: hash::Hasher>(&self, state: &mut H) {
|
||||
self.algorithm.hash(state);
|
||||
self.fingerprint_type.hash(state);
|
||||
self.fingerprint.as_ref().hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl<Octs, Other> PartialEq<Sshfp<Other>> for Sshfp<Octs>
|
||||
where
|
||||
Octs: AsRef<[u8]> + ?Sized,
|
||||
Other: AsRef<[u8]> + ?Sized,
|
||||
{
|
||||
fn eq(&self, other: &Sshfp<Other>) -> bool {
|
||||
self.algorithm.eq(&other.algorithm)
|
||||
&& self.fingerprint_type.eq(&other.fingerprint_type)
|
||||
&& self.fingerprint.as_ref().eq(other.fingerprint.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl<Octs: AsRef<[u8]> + ?Sized> Eq for Sshfp<Octs> {}
|
||||
|
||||
impl<Octs: AsRef<[u8]>> fmt::Display for Sshfp<Octs> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{} {} ( ",
|
||||
u8::from(self.algorithm),
|
||||
u8::from(self.fingerprint_type)
|
||||
)?;
|
||||
base16::display(&self.fingerprint, f)?;
|
||||
write!(f, " )")
|
||||
}
|
||||
}
|
||||
|
||||
impl<Octs: AsRef<[u8]>> fmt::Debug for Sshfp<Octs> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("Sshfp(")?;
|
||||
fmt::Display::fmt(self, f)?;
|
||||
f.write_str(")")
|
||||
}
|
||||
}
|
||||
|
||||
impl<Octs: AsRef<[u8]>> ZonefileFmt for Sshfp<Octs> {
|
||||
fn fmt(&self, p: &mut impl Formatter) -> zonefile_fmt::Result {
|
||||
p.block(|p| {
|
||||
p.write_token(self.algorithm)?;
|
||||
p.write_token(self.fingerprint_type)?;
|
||||
p.write_token(base16::encode_display(&self.fingerprint))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<Octs, Other> PartialOrd<Sshfp<Other>> for Sshfp<Octs>
|
||||
where
|
||||
Octs: AsRef<[u8]>,
|
||||
Other: AsRef<[u8]>,
|
||||
{
|
||||
fn partial_cmp(&self, other: &Sshfp<Other>) -> Option<Ordering> {
|
||||
match self.algorithm.partial_cmp(&other.algorithm) {
|
||||
Some(Ordering::Equal) => {}
|
||||
other => return other,
|
||||
}
|
||||
match self.fingerprint_type.partial_cmp(&other.fingerprint_type) {
|
||||
Some(Ordering::Equal) => {}
|
||||
other => return other,
|
||||
}
|
||||
self.fingerprint
|
||||
.as_ref()
|
||||
.partial_cmp(other.fingerprint.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl<Octs, Other> CanonicalOrd<Sshfp<Other>> for Sshfp<Octs>
|
||||
where
|
||||
Octs: AsRef<[u8]>,
|
||||
Other: AsRef<[u8]>,
|
||||
{
|
||||
fn canonical_cmp(&self, other: &Sshfp<Other>) -> Ordering {
|
||||
match self.algorithm.cmp(&other.algorithm) {
|
||||
Ordering::Equal => {}
|
||||
other => return other,
|
||||
}
|
||||
match self.fingerprint_type.cmp(&other.fingerprint_type) {
|
||||
Ordering::Equal => {}
|
||||
other => return other,
|
||||
}
|
||||
self.fingerprint.as_ref().cmp(other.fingerprint.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl<Octs: AsRef<[u8]>> Ord for Sshfp<Octs> {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
match self.algorithm.cmp(&other.algorithm) {
|
||||
Ordering::Equal => {}
|
||||
other => return other,
|
||||
}
|
||||
match self.fingerprint_type.cmp(&other.fingerprint_type) {
|
||||
Ordering::Equal => {}
|
||||
other => return other,
|
||||
}
|
||||
self.fingerprint.as_ref().cmp(other.fingerprint.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(all(feature = "std", feature = "bytes"))]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::base::rdata::test::{
|
||||
test_compose_parse, test_rdlen, test_scan,
|
||||
};
|
||||
use crate::utils::base16::decode;
|
||||
use std::string::ToString;
|
||||
use std::vec::Vec;
|
||||
|
||||
#[test]
|
||||
// allow redundant_closure because because of lifetime shenanigans
|
||||
// in test_compose_parse(Sshfp::parse), "FnOnce is not general enough"
|
||||
#[allow(clippy::redundant_closure)]
|
||||
fn sshfp_compose_parse_scan() {
|
||||
let algorithm = 1.into();
|
||||
let fingerprint_type = 1.into();
|
||||
let fingerprint_str = "73d3fa022a121062580431316bfe5d56653b91c2";
|
||||
let fingerprint: Vec<u8> = decode(fingerprint_str).unwrap();
|
||||
let rdata = Sshfp::new(algorithm, fingerprint_type, fingerprint);
|
||||
test_rdlen(&rdata);
|
||||
test_compose_parse(&rdata, |parser| Sshfp::parse(parser));
|
||||
test_scan(
|
||||
&[
|
||||
&u8::from(algorithm).to_string(),
|
||||
&u8::from(fingerprint_type).to_string(),
|
||||
fingerprint_str,
|
||||
],
|
||||
Sshfp::scan,
|
||||
&rdata,
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "zonefile")]
|
||||
#[test]
|
||||
fn sshfp_parse_zonefile() {
|
||||
use crate::base::iana::{SshfpAlgorithm, SshfpType};
|
||||
use crate::base::Name;
|
||||
use crate::rdata::ZoneRecordData;
|
||||
use crate::zonefile::inplace::{Entry, Zonefile};
|
||||
|
||||
// section A.1
|
||||
let content = r#"
|
||||
example. 86400 IN SOA ns1 admin 2018031900 (
|
||||
1800 900 604800 86400 )
|
||||
86400 IN NS ns1
|
||||
86400 IN NS ns2
|
||||
86400 IN SSHFP 1 1 (
|
||||
73d3fa022a121062
|
||||
580431316bfe5d56
|
||||
653b91c2 )
|
||||
ns1 3600 IN A 203.0.113.63
|
||||
ns2 3600 IN AAAA 2001:db8::63
|
||||
"#;
|
||||
|
||||
let mut zone = Zonefile::load(&mut content.as_bytes()).unwrap();
|
||||
zone.set_origin(Name::root());
|
||||
while let Some(entry) = zone.next_entry().unwrap() {
|
||||
match entry {
|
||||
Entry::Record(record) => {
|
||||
if record.rtype() != Rtype::SSHFP {
|
||||
continue;
|
||||
}
|
||||
match record.into_data() {
|
||||
ZoneRecordData::Sshfp(rd) => {
|
||||
assert_eq!(SshfpAlgorithm::RSA, rd.algorithm());
|
||||
assert_eq!(
|
||||
SshfpType::SHA1,
|
||||
rd.fingerprint_type()
|
||||
);
|
||||
}
|
||||
_ => panic!(),
|
||||
}
|
||||
}
|
||||
_ => panic!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,8 +29,8 @@
|
||||
//!
|
||||
pub use self::params::{
|
||||
ComposeSvcParamValue, LongSvcParam, ParseSvcParamValue, PushError,
|
||||
SvcParamValue, SvcParams, SvcParamsBuilder, SvcParamsError,
|
||||
UnknownSvcParam, ValueIter,
|
||||
ScanSvcParamValue, SvcParamValue, SvcParams, SvcParamsBuilder,
|
||||
SvcParamsError, UnknownSvcParam, ValueIter,
|
||||
};
|
||||
pub use self::rdata::{Https, HttpsVariant, Svcb, SvcbRdata, SvcbVariant};
|
||||
|
||||
|
||||
+175
-4
@@ -7,18 +7,26 @@
|
||||
// the allow attribute doesn't get copied to the code generated by serde.
|
||||
#![allow(clippy::needless_maybe_sized)]
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
use std::collections::BTreeMap;
|
||||
#[cfg(feature = "std")]
|
||||
use std::string::String;
|
||||
|
||||
use super::value::AllValues;
|
||||
use crate::base::cmp::CanonicalOrd;
|
||||
use crate::base::iana::SvcParamKey;
|
||||
use crate::base::scan::Symbol;
|
||||
use crate::base::scan::{Scanner, ScannerError, Symbol};
|
||||
use crate::base::wire::{Compose, Parse, ParseError};
|
||||
use crate::base::zonefile_fmt::{self, Formatter, ZonefileFmt};
|
||||
use core::cmp::Ordering;
|
||||
use core::marker::PhantomData;
|
||||
#[cfg(feature = "std")]
|
||||
use core::str::FromStr;
|
||||
use core::{cmp, fmt, hash, mem};
|
||||
use octseq::builder::{EmptyBuilder, FromBuilder, OctetsBuilder, ShortBuf};
|
||||
use octseq::octets::{Octets, OctetsFrom, OctetsInto};
|
||||
use octseq::parse::{Parser, ShortInput};
|
||||
use octseq::FreezeBuilder;
|
||||
|
||||
//------------ SvcParams -----------------------------------------------------
|
||||
|
||||
@@ -186,6 +194,133 @@ impl<Octs: AsRef<[u8]>> SvcParams<Octs> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl<Octs: AsRef<[u8]>> SvcParams<Octs> {
|
||||
pub fn scan<S: Scanner<Octets = Octs>>(
|
||||
scanner: &mut S,
|
||||
) -> Result<Self, S::Error> {
|
||||
// SvcParams in presentation format MAY appear in any order, but keys
|
||||
// MUST NOT be repeated.
|
||||
|
||||
// SvcParam = SvcParamKey ["=" SvcParamValue]
|
||||
// SvcParamValue = char-string ; See Appendix A.
|
||||
// value = *OCTET ; Value before key-specific parsing
|
||||
|
||||
// alpha-lc = %x61-7A ; a-z
|
||||
// SvcParamKey = 1*63(alpha-lc / DIGIT / "-")
|
||||
fn allowed_key_charset(ch: u8) -> bool {
|
||||
(0x61..0x7A).contains(&ch)
|
||||
|| (0x30..0x39).contains(&ch)
|
||||
|| 0x2D == ch
|
||||
}
|
||||
|
||||
let mut builder = scanner.octets_builder()?;
|
||||
|
||||
// Loop over tokens. SvcbParams might be split across multiple tokens
|
||||
// if the SvcParamValue is quoted, therefore using custom
|
||||
// scan_svcb_octets scanner function.
|
||||
let mut key_map = BTreeMap::<SvcParamKey, AllValues<Octs>>::new();
|
||||
while scanner.continues() {
|
||||
let mut is_key = true;
|
||||
let mut key_end = None;
|
||||
let mut value_start = None;
|
||||
let octs = scanner.scan_svcb_octets()?;
|
||||
if octs.as_ref().is_empty() {
|
||||
// If someone provides an empty params token, e.g.: SVCB 10 . ""
|
||||
return Err(S::Error::custom("SvcParams cannot be empty"));
|
||||
}
|
||||
|
||||
for (i, &ch) in octs.as_ref().iter().enumerate() {
|
||||
if is_key {
|
||||
if !allowed_key_charset(ch) {
|
||||
if ch == b'=' {
|
||||
key_end = Some(i);
|
||||
is_key = false;
|
||||
} else {
|
||||
return Err(ScannerError::custom(
|
||||
"invalid SvcParamKey",
|
||||
));
|
||||
}
|
||||
}
|
||||
} else if value_start.is_none() {
|
||||
value_start = Some(i);
|
||||
}
|
||||
}
|
||||
|
||||
let param_key = if let Some(key_end) = key_end {
|
||||
SvcParamKey::from_str(&String::from_utf8_lossy(
|
||||
&octs.as_ref()[0..key_end],
|
||||
))
|
||||
} else {
|
||||
SvcParamKey::from_str(&String::from_utf8_lossy(octs.as_ref()))
|
||||
}
|
||||
.map_err(|_| ScannerError::custom("unknown SvcParamKey"))?;
|
||||
|
||||
let param_value = if let Some(value_start) = value_start {
|
||||
let value = &octs.as_ref()[value_start..];
|
||||
AllValues::<Octs>::value_from_scan_octets(
|
||||
scanner, param_key, value,
|
||||
)?
|
||||
.ok_or(S::Error::custom("could not parse SvcParamValue"))?
|
||||
} else {
|
||||
AllValues::<Octs>::value_from_scan_octets(
|
||||
scanner,
|
||||
param_key,
|
||||
&[],
|
||||
)?
|
||||
.ok_or(S::Error::custom(
|
||||
"could not parse SvcParamValue from empty value",
|
||||
))?
|
||||
};
|
||||
|
||||
if key_map.insert(param_key, param_value).is_some() {
|
||||
return Err(S::Error::custom("duplicate SvcParamKey"));
|
||||
}
|
||||
}
|
||||
|
||||
for (param_key, param_value) in &key_map {
|
||||
// https://www.rfc-editor.org/rfc/rfc9460.html#name-rdata-wire-format
|
||||
// When the list of SvcParams is non-empty, it contains a series
|
||||
// of SvcParamKey=SvcParamValue pairs, represented as:
|
||||
// - a 2-octet field containing the SvcParamKey as an integer in
|
||||
// network byte order.
|
||||
param_key
|
||||
.compose(&mut builder)
|
||||
.map_err(|_| S::Error::short_buf())?;
|
||||
|
||||
// - a 2-octet field containing the length of the SvcParamValue as
|
||||
// an integer between 0 and 65535 in network byte order.
|
||||
let value_len = param_value.compose_len();
|
||||
value_len
|
||||
.compose(&mut builder)
|
||||
.map_err(|_| S::Error::short_buf())?;
|
||||
|
||||
// - an octet string of this length whose contents are the
|
||||
// SvcParamValue in a format determined by the SvcParamKey.
|
||||
param_value
|
||||
.compose_value(&mut builder)
|
||||
.map_err(|_| S::Error::short_buf())?;
|
||||
|
||||
// TODO: [...] Other automatically mandatory keys SHOULD NOT
|
||||
// appear in the list either. (Including them wastes space and
|
||||
// otherwise has no effect.) This is only relevant when parsing
|
||||
// as an HTTPS RR.
|
||||
if let AllValues::Mandatory(m) = ¶m_value {
|
||||
for req in m.iter() {
|
||||
if !key_map.contains_key(&req) {
|
||||
return Err(S::Error::custom("all SvcParamKeys listed in mandatory MUST appear in SvcParams"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SvcParams::from_octets(builder.freeze()).map_err(|e| {
|
||||
println!("error in params::from_octets = {}", &e.0);
|
||||
S::Error::custom("invalid SvcParams")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<Octs: ?Sized> SvcParams<Octs> {
|
||||
/// Returns a reference to the underlying octets sequence.
|
||||
pub fn as_octets(&self) -> &Octs {
|
||||
@@ -334,7 +469,7 @@ where
|
||||
|
||||
//--- Display and Debug
|
||||
|
||||
impl<Octs: Octets + ?Sized> fmt::Display for SvcParams<Octs> {
|
||||
impl<Octs: AsRef<[u8]> + ?Sized> fmt::Display for SvcParams<Octs> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut parser = Parser::from_ref(self.as_slice());
|
||||
let mut first = true;
|
||||
@@ -361,7 +496,7 @@ impl<Octs: Octets + ?Sized> fmt::Display for SvcParams<Octs> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<Octs: Octets + ?Sized> fmt::Debug for SvcParams<Octs> {
|
||||
impl<Octs: AsRef<[u8]> + ?Sized> fmt::Debug for SvcParams<Octs> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_tuple("SvcParams")
|
||||
.field(&format_args!("{}", self))
|
||||
@@ -371,7 +506,7 @@ impl<Octs: Octets + ?Sized> fmt::Debug for SvcParams<Octs> {
|
||||
|
||||
//--- ZonefileFmt
|
||||
|
||||
impl<Octs: Octets + ?Sized> ZonefileFmt for SvcParams<Octs> {
|
||||
impl<Octs: AsRef<[u8]> + ?Sized> ZonefileFmt for SvcParams<Octs> {
|
||||
fn fmt(&self, p: &mut impl Formatter) -> zonefile_fmt::Result {
|
||||
p.block(|p| {
|
||||
let mut parser = Parser::from_ref(self.as_slice());
|
||||
@@ -476,6 +611,23 @@ pub trait ParseSvcParamValue<'a, Octs: ?Sized>:
|
||||
) -> Result<Option<Self>, ParseError>;
|
||||
}
|
||||
|
||||
/// A service binding parameter value that can be parse from wire format.
|
||||
pub trait ScanSvcParamValue<
|
||||
SrcOcts: AsRef<[u8]> + ?Sized,
|
||||
Octs: AsRef<[u8]> + ?Sized,
|
||||
>: SvcParamValue + Sized
|
||||
{
|
||||
/// Scan a parameter value from octets from presentation format.
|
||||
///
|
||||
/// The method should return `Ok(None)` if the type cannot parse values
|
||||
/// with `key`. It should return an error if parsing fails.
|
||||
fn value_from_scan_octets<S: Scanner<Octets = Octs>>(
|
||||
scanner: &mut S,
|
||||
key: SvcParamKey,
|
||||
octs: &SrcOcts,
|
||||
) -> Result<Option<Self>, S::Error>;
|
||||
}
|
||||
|
||||
/// A service binding parameter value that can be composed into wire format.
|
||||
///
|
||||
/// All value types need to be able to calculate the length of their
|
||||
@@ -658,6 +810,25 @@ impl<'a, Octs: Octets + ?Sized> ParseSvcParamValue<'a, Octs>
|
||||
}
|
||||
}
|
||||
|
||||
impl<SrcOcts, Octs> ScanSvcParamValue<SrcOcts, Octs> for UnknownSvcParam<Octs>
|
||||
where
|
||||
Octs: AsRef<[u8]>,
|
||||
SrcOcts: AsRef<[u8]> + ?Sized,
|
||||
{
|
||||
fn value_from_scan_octets<S: Scanner<Octets = Octs>>(
|
||||
scanner: &mut S,
|
||||
key: SvcParamKey,
|
||||
octs: &SrcOcts,
|
||||
) -> Result<Option<Self>, S::Error> {
|
||||
let mut tmp = scanner.octets_builder()?;
|
||||
tmp.append_slice(octs.as_ref())
|
||||
.map_err(|_| S::Error::short_buf())?;
|
||||
Self::new(key, tmp.freeze()).map(Some).map_err(|_| {
|
||||
S::Error::custom("SvcParamValue for unknown param too long")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<Octs: AsRef<[u8]>> ComposeSvcParamValue for UnknownSvcParam<Octs> {
|
||||
fn compose_len(&self) -> u16 {
|
||||
u16::try_from(self.as_slice().len()).expect("long value")
|
||||
|
||||
+323
-3
@@ -9,6 +9,9 @@ use crate::base::name::{FlattenInto, ParsedName, ToName};
|
||||
use crate::base::rdata::{
|
||||
ComposeRecordData, LongRecordData, ParseRecordData, RecordData,
|
||||
};
|
||||
#[cfg(feature = "std")]
|
||||
use crate::base::scan::Scan;
|
||||
use crate::base::scan::{Scanner, ScannerError};
|
||||
use crate::base::wire::{Compose, Composer, Parse, ParseError};
|
||||
use crate::base::zonefile_fmt::{self, Formatter, ZonefileFmt};
|
||||
use core::marker::PhantomData;
|
||||
@@ -179,6 +182,49 @@ impl<Variant, Octs: AsRef<[u8]>> SvcbRdata<Variant, Octs, ParsedName<Octs>> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<Octs: AsRef<[u8]>, Name: ToName> SvcbRdata<SvcbVariant, Octs, Name> {
|
||||
pub fn scan<S: Scanner<Name = Name, Octets = Octs>>(
|
||||
scanner: &mut S,
|
||||
) -> Result<Self, S::Error> {
|
||||
#[cfg(feature = "std")]
|
||||
{
|
||||
let priority = u16::scan(scanner)?;
|
||||
let target = scanner.scan_name()?;
|
||||
let params = SvcParams::scan(scanner)?;
|
||||
|
||||
Self::new(priority, target, params)
|
||||
.map_err(|_| S::Error::custom("SVCB record too long"))
|
||||
}
|
||||
#[cfg(not(feature = "std"))]
|
||||
{
|
||||
let _ = scanner;
|
||||
Err(S::Error::custom("zonefile parsing of SVCB RRs is not implemented without the domain std feature"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Octs: AsRef<[u8]>, Name: ToName> SvcbRdata<HttpsVariant, Octs, Name> {
|
||||
pub fn scan<S: Scanner<Name = Name, Octets = Octs>>(
|
||||
scanner: &mut S,
|
||||
) -> Result<Self, S::Error> {
|
||||
#[cfg(feature = "std")]
|
||||
{
|
||||
let priority = u16::scan(scanner)?;
|
||||
let target = scanner.scan_name()?;
|
||||
// TODO: The "automatically mandatory" keys (Section 8) are "port" and "no-default-alpn".
|
||||
let params = SvcParams::scan(scanner)?;
|
||||
|
||||
Self::new(priority, target, params)
|
||||
.map_err(|_| S::Error::custom("HTTPS record too long"))
|
||||
}
|
||||
#[cfg(not(feature = "std"))]
|
||||
{
|
||||
let _ = scanner;
|
||||
Err(S::Error::custom("zonefile parsing of HTTPS RRs is not implemented without the domain std feature"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Variant, Octs, Name> SvcbRdata<Variant, Octs, Name> {
|
||||
/// Returns the priority.
|
||||
pub fn priority(&self) -> u16 {
|
||||
@@ -467,7 +513,7 @@ where
|
||||
|
||||
impl<Variant, Octs, Name> fmt::Display for SvcbRdata<Variant, Octs, Name>
|
||||
where
|
||||
Octs: Octets,
|
||||
Octs: AsRef<[u8]>,
|
||||
Name: fmt::Display,
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
@@ -477,7 +523,7 @@ where
|
||||
|
||||
impl<Variant, Octs, Name> fmt::Debug for SvcbRdata<Variant, Octs, Name>
|
||||
where
|
||||
Octs: Octets,
|
||||
Octs: AsRef<[u8]>,
|
||||
Name: fmt::Debug,
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
@@ -493,7 +539,7 @@ where
|
||||
|
||||
impl<Variant, Octs, Name> ZonefileFmt for SvcbRdata<Variant, Octs, Name>
|
||||
where
|
||||
Octs: Octets,
|
||||
Octs: AsRef<[u8]>,
|
||||
Name: ToName,
|
||||
{
|
||||
fn fmt(&self, p: &mut impl Formatter) -> zonefile_fmt::Result {
|
||||
@@ -603,3 +649,277 @@ mod test {
|
||||
assert_eq!(rdata.as_ref(), buf.as_ref());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(feature = "zonefile")]
|
||||
mod svcb_zonefile_tests {
|
||||
use super::*;
|
||||
use crate::base::iana::Class;
|
||||
use crate::zonefile::inplace::{self, Zonefile};
|
||||
use octseq::array::Array;
|
||||
|
||||
type Octets512 = Array<512>;
|
||||
|
||||
#[track_caller]
|
||||
/// A helper function that takes a single resource record in zonefile
|
||||
/// format as input for the Zonefile parser (because only the
|
||||
/// inplace::Zonefile parser has SVCB implemented), and compares it to the
|
||||
/// expected rdata octets.
|
||||
fn svcb_zonefile_parse_expect(
|
||||
rr: impl AsRef<[u8]>,
|
||||
expected: impl AsRef<[u8]>,
|
||||
is_https: bool,
|
||||
) {
|
||||
let mut zonefile = Zonefile::from(rr.as_ref());
|
||||
zonefile.set_default_class(Class::IN);
|
||||
let inplace::Entry::Record(scanned_rr) =
|
||||
zonefile.next_entry().unwrap().unwrap()
|
||||
else {
|
||||
panic!()
|
||||
};
|
||||
|
||||
let mut buf = Octets512::new();
|
||||
if is_https {
|
||||
if let crate::rdata::ZoneRecordData::Https(scanned_rdata) =
|
||||
scanned_rr.data()
|
||||
{
|
||||
scanned_rdata.compose_rdata(&mut buf).unwrap();
|
||||
} else {
|
||||
panic!()
|
||||
}
|
||||
} else if let crate::rdata::ZoneRecordData::Svcb(scanned_rdata) =
|
||||
scanned_rr.data()
|
||||
{
|
||||
scanned_rdata.compose_rdata(&mut buf).unwrap();
|
||||
} else {
|
||||
panic!()
|
||||
}
|
||||
|
||||
assert_eq!(buf.as_ref(), expected.as_ref());
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
/// A helper function that takes a single resource record in zonefile
|
||||
/// format as input for the Zonefile parser (because only the
|
||||
/// inplace::Zonefile parser has SVCB implemented), and only unwraps the
|
||||
/// result. This is used by the should panic parsing tests.
|
||||
fn svcb_zonefile_parse_only(
|
||||
rr: impl AsRef<[u8]>,
|
||||
) -> Result<Option<inplace::Entry>, inplace::Error> {
|
||||
let mut zonefile = Zonefile::from(rr.as_ref());
|
||||
zonefile.set_default_class(Class::IN);
|
||||
zonefile.next_entry()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vectors_alias_zonefile() {
|
||||
// [RFC9460 Figure 2](https://www.rfc-editor.org/rfc/rfc9460.html#name-aliasmode-4)
|
||||
svcb_zonefile_parse_expect(
|
||||
b"example.com. HTTPS 0 foo.example.com.\n",
|
||||
b"\x00\x00\x03foo\x07example\x03com\x00",
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vectors_target_name_dot_zonefile() {
|
||||
// [RFC9460 Figure 3](https://www.rfc-editor.org/rfc/rfc9460.html#name-targetname-is)
|
||||
svcb_zonefile_parse_expect(
|
||||
b"example.com. SVCB 1 .\n",
|
||||
b"\x00\x01\x00",
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vectors_specifies_a_port_zonefile() {
|
||||
// [RFC9460 Figure 4](https://www.rfc-editor.org/rfc/rfc9460.html#name-specifies-a-port)
|
||||
svcb_zonefile_parse_expect(
|
||||
b"example.com. SVCB 16 foo.example.com. port=53\n",
|
||||
b"\x00\x10\x03foo\x07example\x03com\x00\x00\x03\x00\x02\x00\x35",
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vectors_generic_key_unquoted_value_zonefile() {
|
||||
// [RFC9460 Figure 5](https://www.rfc-editor.org/rfc/rfc9460.html#name-a-generic-key-and-unquoted-)
|
||||
svcb_zonefile_parse_expect(
|
||||
b"example.com. SVCB 1 foo.example.com. key667=hello\n",
|
||||
b"\x00\x01\x03foo\x07example\x03com\x00\x02\x9b\x00\x05hello",
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vectors_generic_key_quoted_value_decimal_escape_zonefile() {
|
||||
// [RFC9460 Figure 6](https://www.rfc-editor.org/rfc/rfc9460.html#name-a-generic-key-and-quoted-va)
|
||||
svcb_zonefile_parse_expect(
|
||||
b"example.com. SVCB 1 foo.example.com. key667=\"hello\\210qoo\"\n",
|
||||
b"\x00\x01\x03foo\x07example\x03com\x00\x02\x9b\x00\x09hello\xd2qoo",
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vectors_two_quoted_ipv6_hints_zonefile() {
|
||||
// [RFC9460 Figure 7](https://www.rfc-editor.org/rfc/rfc9460.html#name-two-quoted-ipv6-hints)
|
||||
svcb_zonefile_parse_expect(
|
||||
b"example.com. SVCB 1 foo.example.com. ( ipv6hint=\"2001:db8::1,2001:db8::53:1\" )\n",
|
||||
b"\x00\x01\
|
||||
\x03foo\x07example\x03com\x00\
|
||||
\x00\x06\
|
||||
\x00\x20\
|
||||
\x20\x01\x0d\xb8\x00\x00\x00\x00\
|
||||
\x00\x00\x00\x00\x00\x00\x00\x01\
|
||||
\x20\x01\x0d\xb8\x00\x00\x00\x00\
|
||||
\x00\x00\x00\x00\x00\x53\x00\x01",
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vectors_ipv6_hint_embedded_ipv4_zonefile() {
|
||||
// [RFC9460 Figure 8](https://www.rfc-editor.org/rfc/rfc9460.html#name-an-ipv6-hint-using-the-embe)
|
||||
svcb_zonefile_parse_expect(
|
||||
b"example.com. SVCB 1 example.com. ( ipv6hint=\"2001:db8:122:344::192.0.2.33\" )\n",
|
||||
b"\x00\x01\
|
||||
\x07example\x03com\x00\
|
||||
\x00\x06\
|
||||
\x00\x10\
|
||||
\x20\x01\x0d\xb8\x01\x22\x03\x44\
|
||||
\x00\x00\x00\x00\xc0\x00\x02\x21",
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vectors_ordered_in_wireformat_zonefile() {
|
||||
// [RFC9460 Figure 9](https://www.rfc-editor.org/rfc/rfc9460.html#name-svcparamkey-ordering-is-arb)
|
||||
svcb_zonefile_parse_expect(
|
||||
b"example.com. SVCB 16 foo.example.org. (
|
||||
alpn=h2,h3-19 mandatory=ipv4hint,alpn
|
||||
ipv4hint=192.0.2.1
|
||||
)\n",
|
||||
b"\x00\x10\
|
||||
\x03foo\x07example\x03org\x00\
|
||||
\x00\x00\
|
||||
\x00\x04\
|
||||
\x00\x01\
|
||||
\x00\x04\
|
||||
\x00\x01\
|
||||
\x00\x09\
|
||||
\x02\
|
||||
h2\
|
||||
\x05\
|
||||
h3-19\
|
||||
\x00\x04\
|
||||
\x00\x04\
|
||||
\xc0\x00\x02\x01",
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
// Disabled until SvcParamValueScanIter::next() is implemented/fixed to
|
||||
// evaluate r"\\" and r"\," correctly.
|
||||
// #[test]
|
||||
// fn test_vectors_alpn_with_escapes_quoted_zonefile() {
|
||||
// // [RFC9460 Figure 10](https://www.rfc-editor.org/rfc/rfc9460.html#name-an-alpn-value-with-an-escap)
|
||||
// let rr = br#"example.com. SVCB 16 foo.example.org. alpn="f\\\\oo\\,bar,h2""#;
|
||||
// let rr = [rr.as_ref(), b"\n"].concat();
|
||||
// svcb_zonefile_parse_expect(
|
||||
// rr,
|
||||
// b"\x00\x10\
|
||||
// \x03foo\x07example\x03org\x00\
|
||||
// \x00\x01\
|
||||
// \x00\x0c\
|
||||
// \x08\
|
||||
// f\\oo,bar\
|
||||
// \x02\
|
||||
// h2",
|
||||
// false,
|
||||
// )
|
||||
// }
|
||||
|
||||
// Disabled until SvcParamValueScanIter::next() is implemented/fixed to
|
||||
// evaluate r"\\" and r"\," correctly.
|
||||
// #[test]
|
||||
// fn test_vectors_alpn_with_escapes_unquoted_zonefile() {
|
||||
// // [RFC9460 Figure 10](https://www.rfc-editor.org/rfc/rfc9460.html#name-an-alpn-value-with-an-escap)
|
||||
// let rr = br"example.com. SVCB 16 foo.example.org. alpn=f\\\092oo\092,bar,h2";
|
||||
// let rr = [rr.as_ref(), b"\n"].concat();
|
||||
// svcb_zonefile_parse_expect(
|
||||
// rr,
|
||||
// b"\x00\x10\
|
||||
// \x03foo\x07example\x03org\x00\
|
||||
// \x00\x01\
|
||||
// \x00\x0c\
|
||||
// \x08\
|
||||
// f\\oo,bar\
|
||||
// \x02\
|
||||
// h2",
|
||||
// false,
|
||||
// )
|
||||
// }
|
||||
|
||||
#[test]
|
||||
fn test_vectors_multiple_instances_of_same_key_zonefile() {
|
||||
// [RFC9460 Figure 11](https://www.rfc-editor.org/rfc/rfc9460.html#name-multiple-instances-of-the-s)
|
||||
svcb_zonefile_parse_only(
|
||||
b"example.com. SVCB 1 foo.example.com. (
|
||||
key123=abc key123=def )\n",
|
||||
)
|
||||
.unwrap_err();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vectors_missing_paramvalues_zonefile() {
|
||||
// [RFC9460 Figure 12](https://www.rfc-editor.org/rfc/rfc9460.html#name-missing-svcparamvalues-that)
|
||||
for rr in [
|
||||
b"example.com. SVCB 1 foo.example.com. mandatory\n".as_ref(),
|
||||
b"example.com. SVCB 2 foo.example.com. alpn\n".as_ref(),
|
||||
b"example.com. SVCB 3 foo.example.com. port\n".as_ref(),
|
||||
b"example.com. SVCB 4 foo.example.com. ipv4hint\n".as_ref(),
|
||||
b"example.com. SVCB 5 foo.example.com. ipv6hint\n".as_ref(),
|
||||
] {
|
||||
svcb_zonefile_parse_only(rr).unwrap_err();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vectors_empty_no_default_alpn_zonefile() {
|
||||
// [RFC9460 Figure 13](https://www.rfc-editor.org/rfc/rfc9460.html#name-the-no-default-alpn-svcpara)
|
||||
svcb_zonefile_parse_only(
|
||||
b"example.com. SVCB 1 foo.example.com. no-default-alpn=abc\n",
|
||||
)
|
||||
.unwrap_err();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vectors_mandatory_param_missing_zonefile() {
|
||||
// [RFC9460 Figure 14](https://www.rfc-editor.org/rfc/rfc9460.html#name-a-mandatory-svcparam-is-mis)
|
||||
svcb_zonefile_parse_only(
|
||||
b"example.com. SVCB 1 foo.example.com. mandatory=key123\n",
|
||||
)
|
||||
.unwrap_err();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vectors_mandatory_not_in_mandatory_zonefile() {
|
||||
// [RFC9460 Figure 15](https://www.rfc-editor.org/rfc/rfc9460.html#name-the-mandatory-svcparamkey-m)
|
||||
svcb_zonefile_parse_only(
|
||||
b"example.com. SVCB 1 foo.example.com. mandatory=mandatory\n",
|
||||
)
|
||||
.unwrap_err();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vectors_multiple_of_same_key_in_mandatory_zonefile() {
|
||||
// [RFC9460 Figure 16](https://www.rfc-editor.org/rfc/rfc9460.html#name-multiple-instances-of-the-sa)
|
||||
svcb_zonefile_parse_only(
|
||||
b"example.com. SVCB 1 foo.example.com. (
|
||||
mandatory=key123,key123 key123=abc )\n",
|
||||
)
|
||||
.unwrap_err();
|
||||
}
|
||||
}
|
||||
|
||||
+771
-2
@@ -1,9 +1,18 @@
|
||||
#[cfg(feature = "std")]
|
||||
use std::collections::BTreeSet;
|
||||
#[cfg(feature = "std")]
|
||||
use std::collections::HashSet;
|
||||
|
||||
use super::{
|
||||
ComposeSvcParamValue, LongSvcParam, ParseSvcParamValue, PushError,
|
||||
SvcParamValue, SvcParams, SvcParamsBuilder, UnknownSvcParam,
|
||||
ScanSvcParamValue, SvcParamValue, SvcParams, SvcParamsBuilder,
|
||||
UnknownSvcParam,
|
||||
};
|
||||
use crate::base::iana::SvcParamKey;
|
||||
use crate::base::net::{Ipv4Addr, Ipv6Addr};
|
||||
use crate::base::scan::{
|
||||
ConvertSymbols, EntrySymbol, Scanner, ScannerError, Symbol,
|
||||
};
|
||||
use crate::base::wire::{Compose, Parse, ParseError};
|
||||
use crate::utils::base64;
|
||||
use core::fmt::Write as _;
|
||||
@@ -131,6 +140,37 @@ macro_rules! values_enum {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl<SrcOcts, Octs> ScanSvcParamValue<SrcOcts, Octs>
|
||||
for AllValues<Octs>
|
||||
where
|
||||
Octs: AsRef<[u8]>,
|
||||
SrcOcts: AsRef<[u8]> + ?Sized,
|
||||
{
|
||||
fn value_from_scan_octets<S: Scanner<Octets = Octs>>(
|
||||
scanner: &mut S,
|
||||
key: SvcParamKey,
|
||||
octs: &SrcOcts,
|
||||
) -> Result<Option<Self>, S::Error> {
|
||||
match key {
|
||||
$(
|
||||
$type::KEY => {
|
||||
$type::value_from_scan_octets(
|
||||
scanner,
|
||||
key,
|
||||
octs
|
||||
).map(|res| Some(Self::$type(res.unwrap())))
|
||||
}
|
||||
)+
|
||||
_ => {
|
||||
UnknownSvcParam::value_from_scan_octets(
|
||||
scanner, key, octs
|
||||
).map(|res| res.map(Self::Unknown))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Octs: AsRef<[u8]>> ComposeSvcParamValue for AllValues<Octs> {
|
||||
fn compose_len(&self) -> u16 {
|
||||
match self {
|
||||
@@ -215,6 +255,8 @@ values_enum! {
|
||||
Ipv4Hint<Octs>,
|
||||
Ipv6Hint<Octs>,
|
||||
DohPath<Octs>,
|
||||
Ohttp,
|
||||
TlsSupportedGroups<Octs>,
|
||||
}
|
||||
|
||||
//============ Individual Value Types ========================================
|
||||
@@ -474,9 +516,61 @@ impl<Octs: AsRef<[u8]>> Mandatory<Octs> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl<SrcOcts, Octs> ScanSvcParamValue<SrcOcts, Octs> for Mandatory<Octs>
|
||||
where
|
||||
Octs: AsRef<[u8]>,
|
||||
SrcOcts: AsRef<[u8]> + ?Sized,
|
||||
{
|
||||
fn value_from_scan_octets<S: Scanner<Octets = Octs>>(
|
||||
scanner: &mut S,
|
||||
key: SvcParamKey,
|
||||
octs: &SrcOcts,
|
||||
) -> Result<Option<Self>, S::Error> {
|
||||
if key == Mandatory::KEY {
|
||||
let mut tmp = scanner.octets_builder()?;
|
||||
let mut iter = SvcParamValueScanIter::from_slice(octs.as_ref());
|
||||
// keys must be in order, therefore BTreeSet instead of HashSet
|
||||
let mut keys = BTreeSet::<SvcParamKey>::new();
|
||||
while let Some(item) = iter.next_no_escapes().map_err(|_| {
|
||||
S::Error::custom("no escape sequences allowed in mandatory")
|
||||
})? {
|
||||
let k =
|
||||
SvcParamKey::from_bytes(item).ok_or(S::Error::custom(
|
||||
"invalid key listed in SvcParamKey mandatory",
|
||||
))?;
|
||||
if k == SvcParamKey::MANDATORY {
|
||||
return Err(S::Error::custom(
|
||||
// https://www.rfc-editor.org/rfc/rfc9460.html#section-8-8
|
||||
"the key 'mandatory' MUST NOT appear in the mandatory keys list",
|
||||
));
|
||||
}
|
||||
if !keys.insert(k) {
|
||||
return Err(S::Error::custom(
|
||||
"mandatory contains duplicate keys",
|
||||
));
|
||||
}
|
||||
}
|
||||
if keys.is_empty() {
|
||||
return Err(S::Error::custom(
|
||||
"mandatory requires at least one value",
|
||||
));
|
||||
}
|
||||
for k in keys {
|
||||
k.compose(&mut tmp).map_err(|_| S::Error::short_buf())?;
|
||||
}
|
||||
Ok(Some(Self::from_octets(tmp.freeze()).map_err(|_| {
|
||||
S::Error::custom("invalid svc param value for mandatory")
|
||||
})?))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//--- Iterator
|
||||
|
||||
impl<Octs: Octets + ?Sized> Iterator for MandatoryIter<'_, Octs> {
|
||||
impl<Octs: AsRef<[u8]> + ?Sized> Iterator for MandatoryIter<'_, Octs> {
|
||||
type Item = SvcParamKey;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
@@ -608,6 +702,55 @@ impl<Octs: AsRef<[u8]>> Alpn<Octs> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<SrcOcts, Octs> ScanSvcParamValue<SrcOcts, Octs> for Alpn<Octs>
|
||||
where
|
||||
Octs: AsRef<[u8]>,
|
||||
SrcOcts: AsRef<[u8]> + ?Sized,
|
||||
{
|
||||
fn value_from_scan_octets<S: Scanner<Octets = Octs>>(
|
||||
scanner: &mut S,
|
||||
key: SvcParamKey,
|
||||
octs: &SrcOcts,
|
||||
) -> Result<Option<Self>, S::Error> {
|
||||
if key == Alpn::KEY {
|
||||
let mut tmp = scanner.octets_builder()?;
|
||||
let mut iter = SvcParamValueScanIter::from_slice(octs.as_ref());
|
||||
let mut at_least_one = false;
|
||||
// For now: "Zone-file implementations MAY disallow the "," and
|
||||
// "\" characters in ALPN IDs instead of implementing the
|
||||
// value-list escaping procedure, relying on the opaque key format
|
||||
// (e.g., key1=\002h2) in the event that these characters are
|
||||
// needed."
|
||||
// (https://www.rfc-editor.org/rfc/rfc9460.html#section-7.1.1-3)
|
||||
// TODO: implement ALPN escaping
|
||||
// while let Some(item) = iter.next() {
|
||||
while let Some(item) = iter.next_no_escapes().map_err(|_| {
|
||||
S::Error::custom("this implementation does not allow escape sequences in alpn")
|
||||
})? {
|
||||
at_least_one = true;
|
||||
let len: u8 = item.len().try_into().map_err(|_| {
|
||||
S::Error::custom("SvcParamValue is too long")
|
||||
})?;
|
||||
tmp.append_slice(&[len])
|
||||
.map_err(|_| S::Error::short_buf())?;
|
||||
tmp.append_slice(item).map_err(|_| S::Error::short_buf())?;
|
||||
}
|
||||
|
||||
if !at_least_one {
|
||||
return Err(S::Error::custom(
|
||||
"expected at least one alpn-id value",
|
||||
));
|
||||
}
|
||||
Ok(Some(Self::from_octets(tmp.freeze()).map_err(|_| {
|
||||
S::Error::custom("invalid svc param value for alpn")
|
||||
})?))
|
||||
} else {
|
||||
// TODO: why is it ok none if the key is wrong? (stolen from parse_value)
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//--- Iterator
|
||||
|
||||
impl<'a, Octs: Octets + ?Sized> Iterator for AlpnIter<'a, Octs> {
|
||||
@@ -798,6 +941,29 @@ impl<'a, Octs: Octets + ?Sized> ParseSvcParamValue<'a, Octs>
|
||||
}
|
||||
}
|
||||
|
||||
impl<SrcOcts, Octs> ScanSvcParamValue<SrcOcts, Octs> for NoDefaultAlpn
|
||||
where
|
||||
Octs: AsRef<[u8]>,
|
||||
SrcOcts: AsRef<[u8]> + ?Sized,
|
||||
{
|
||||
fn value_from_scan_octets<S: Scanner<Octets = Octs>>(
|
||||
_scanner: &mut S,
|
||||
key: SvcParamKey,
|
||||
octs: &SrcOcts,
|
||||
) -> Result<Option<Self>, S::Error> {
|
||||
if key == Self::KEY {
|
||||
if !octs.as_ref().is_empty() {
|
||||
return Err(S::Error::custom(
|
||||
"no-default-alpn takes no values",
|
||||
));
|
||||
}
|
||||
Ok(Some(Self))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ComposeSvcParamValue for NoDefaultAlpn {
|
||||
fn compose_len(&self) -> u16 {
|
||||
0
|
||||
@@ -890,6 +1056,35 @@ impl<'a, Octs: Octets + ?Sized> ParseSvcParamValue<'a, Octs> for Port {
|
||||
}
|
||||
}
|
||||
|
||||
impl<SrcOcts, Octs> ScanSvcParamValue<SrcOcts, Octs> for Port
|
||||
where
|
||||
Octs: AsRef<[u8]>,
|
||||
SrcOcts: AsRef<[u8]> + ?Sized,
|
||||
{
|
||||
fn value_from_scan_octets<S: Scanner<Octets = Octs>>(
|
||||
_scanner: &mut S,
|
||||
key: SvcParamKey,
|
||||
octs: &SrcOcts,
|
||||
) -> Result<Option<Self>, S::Error> {
|
||||
if key == Self::KEY {
|
||||
if octs.as_ref().is_empty() {
|
||||
return Err(S::Error::custom("port requires a value"));
|
||||
}
|
||||
let s = str::from_utf8(octs.as_ref()).map_err(|_| {
|
||||
S::Error::custom("port value must be valid utf-8")
|
||||
})?;
|
||||
let port = s.parse::<u16>().map_err(|_| {
|
||||
S::Error::custom(
|
||||
"port value must be a 16-bit unsigned decimal number",
|
||||
)
|
||||
})?;
|
||||
Ok(Some(Self::new(port)))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ComposeSvcParamValue for Port {
|
||||
fn compose_len(&self) -> u16 {
|
||||
u16::COMPOSE_LEN
|
||||
@@ -981,6 +1176,53 @@ impl<Octs: AsRef<[u8]>> Ech<Octs> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<SrcOcts, Octs> ScanSvcParamValue<SrcOcts, Octs> for Ech<Octs>
|
||||
where
|
||||
Octs: AsRef<[u8]>,
|
||||
SrcOcts: AsRef<[u8]> + ?Sized,
|
||||
{
|
||||
fn value_from_scan_octets<S: Scanner<Octets = Octs>>(
|
||||
scanner: &mut S,
|
||||
key: SvcParamKey,
|
||||
octs: &SrcOcts,
|
||||
) -> Result<Option<Self>, S::Error> {
|
||||
if key == Ech::KEY {
|
||||
if octs.as_ref().is_empty() {
|
||||
return Err(S::Error::custom("ech requires as value"));
|
||||
}
|
||||
let mut builder = scanner.octets_builder()?;
|
||||
let mut convert = base64::SymbolConverter::new();
|
||||
for ch in octs.as_ref() {
|
||||
if let Some(data) = convert.process_symbol(
|
||||
EntrySymbol::from(Symbol::from_octet(*ch)),
|
||||
)? {
|
||||
builder
|
||||
.append_slice(data)
|
||||
.map_err(|_| S::Error::short_buf())?;
|
||||
}
|
||||
}
|
||||
|
||||
// if let Some(data) = convert.process_tail()? {
|
||||
if let Some(data) = <base64::SymbolConverter as ConvertSymbols<
|
||||
EntrySymbol,
|
||||
<S as Scanner>::Error,
|
||||
>>::process_tail(&mut convert)?
|
||||
{
|
||||
builder
|
||||
.append_slice(data)
|
||||
.map_err(|_| S::Error::short_buf())?;
|
||||
}
|
||||
let dec = builder.freeze();
|
||||
|
||||
Ok(Some(Self::from_octets(dec).map_err(|_| {
|
||||
S::Error::custom("invalid ech param value")
|
||||
})?))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//--- Display
|
||||
|
||||
impl<Octs: AsRef<[u8]> + ?Sized> fmt::Display for Ech<Octs> {
|
||||
@@ -1099,6 +1341,47 @@ impl<Octs: AsRef<[u8]>> Ipv4Hint<Octs> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<SrcOcts, Octs> ScanSvcParamValue<SrcOcts, Octs> for Ipv4Hint<Octs>
|
||||
where
|
||||
Octs: AsRef<[u8]>,
|
||||
SrcOcts: AsRef<[u8]> + ?Sized,
|
||||
{
|
||||
fn value_from_scan_octets<S: Scanner<Octets = Octs>>(
|
||||
scanner: &mut S,
|
||||
key: SvcParamKey,
|
||||
octs: &SrcOcts,
|
||||
) -> Result<Option<Self>, S::Error> {
|
||||
if key == Ipv4Hint::KEY {
|
||||
let mut tmp = scanner.octets_builder()?;
|
||||
let mut iter = SvcParamValueScanIter::from_slice(octs.as_ref());
|
||||
while let Some(item) = iter.next_no_escapes().map_err(|_| {
|
||||
S::Error::custom("no escape sequences allowed in ipv4hint")
|
||||
})? {
|
||||
let ip = Ipv4Addr::from_str(str::from_utf8(item).map_err(
|
||||
|_| S::Error::custom("invalid utf-8 in ipv4hint param"),
|
||||
)?)
|
||||
.map_err(|_| {
|
||||
S::Error::custom("invalid ipv4 in ipv4hint param")
|
||||
})?;
|
||||
tmp.append_slice(&ip.octets())
|
||||
.map_err(|_| S::Error::short_buf())?;
|
||||
}
|
||||
// tmp.append_slice();
|
||||
let ipv4hint = Self::from_octets(tmp.freeze()).map_err(|_| {
|
||||
S::Error::custom("invalid svc param value for ipv4hint")
|
||||
})?;
|
||||
if ipv4hint.as_slice().is_empty() {
|
||||
return Err(S::Error::custom(
|
||||
"ipv4hint requires at least one value",
|
||||
));
|
||||
}
|
||||
Ok(Some(ipv4hint))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Octs: Octets + ?Sized> Iterator for Ipv4HintIter<'_, Octs> {
|
||||
type Item = Ipv4Addr;
|
||||
|
||||
@@ -1247,6 +1530,46 @@ impl<Octs: AsRef<[u8]>> Ipv6Hint<Octs> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<SrcOcts, Octs> ScanSvcParamValue<SrcOcts, Octs> for Ipv6Hint<Octs>
|
||||
where
|
||||
Octs: AsRef<[u8]>,
|
||||
SrcOcts: AsRef<[u8]> + ?Sized,
|
||||
{
|
||||
fn value_from_scan_octets<S: Scanner<Octets = Octs>>(
|
||||
scanner: &mut S,
|
||||
key: SvcParamKey,
|
||||
octs: &SrcOcts,
|
||||
) -> Result<Option<Self>, S::Error> {
|
||||
if key == Ipv6Hint::KEY {
|
||||
let mut tmp = scanner.octets_builder()?;
|
||||
let mut iter = SvcParamValueScanIter::from_slice(octs.as_ref());
|
||||
while let Some(item) = iter.next_no_escapes().map_err(|_| {
|
||||
S::Error::custom("no escape sequences allowed in ipv6hint")
|
||||
})? {
|
||||
let ip = Ipv6Addr::from_str(str::from_utf8(item).map_err(
|
||||
|_| S::Error::custom("invalid utf-8 in ipv6hint param"),
|
||||
)?)
|
||||
.map_err(|_| {
|
||||
S::Error::custom("invalid ipv6 in ipv6hint param")
|
||||
})?;
|
||||
tmp.append_slice(&ip.octets())
|
||||
.map_err(|_| S::Error::short_buf())?;
|
||||
}
|
||||
let ipv6hint = Self::from_octets(tmp.freeze()).map_err(|_| {
|
||||
S::Error::custom("invalid svc param value for ipv6hint")
|
||||
})?;
|
||||
if ipv6hint.as_slice().is_empty() {
|
||||
return Err(S::Error::custom(
|
||||
"ipv6hint requires at least one value",
|
||||
));
|
||||
}
|
||||
Ok(Some(ipv6hint))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//--- Iterator
|
||||
|
||||
impl<Octs: Octets + ?Sized> Iterator for Ipv6HintIter<'_, Octs> {
|
||||
@@ -1368,6 +1691,32 @@ impl<Octs: AsRef<[u8]>> DohPath<Octs> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<SrcOcts, Octs> ScanSvcParamValue<SrcOcts, Octs> for DohPath<Octs>
|
||||
where
|
||||
Octs: AsRef<[u8]>,
|
||||
SrcOcts: AsRef<[u8]> + ?Sized,
|
||||
{
|
||||
fn value_from_scan_octets<S: Scanner<Octets = Octs>>(
|
||||
scanner: &mut S,
|
||||
key: SvcParamKey,
|
||||
octs: &SrcOcts,
|
||||
) -> Result<Option<Self>, S::Error> {
|
||||
if key == DohPath::KEY {
|
||||
let _ = str::from_utf8(octs.as_ref()).map_err(|_| {
|
||||
S::Error::custom("dohpath must be valid UTF-8")
|
||||
})?;
|
||||
let mut tmp = scanner.octets_builder()?;
|
||||
tmp.append_slice(octs.as_ref())
|
||||
.map_err(|_| S::Error::short_buf())?;
|
||||
Ok(Some(Self::from_octets(tmp.freeze()).map_err(|_| {
|
||||
S::Error::custom("invalid svc param value for dohpath")
|
||||
})?))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//--- TryFrom and FromStr
|
||||
|
||||
impl<Octs: AsRef<[u8]>> TryFrom<Str<Octs>> for DohPath<Octs> {
|
||||
@@ -1456,6 +1805,426 @@ impl<Octs: OctetsBuilder + AsRef<[u8]> + AsMut<[u8]>> SvcParamsBuilder<Octs> {
|
||||
}
|
||||
}
|
||||
|
||||
//------------ Ohttp -------------------------------------------------
|
||||
|
||||
/// A signal that Oblivious HTTP is supported.
|
||||
///
|
||||
/// The "ohttp" SvcParamKey is used to indicate that a service described in
|
||||
/// a SVCB RR can be accessed as a target using an associated gateway.
|
||||
///
|
||||
/// This value is always empty.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct Ohttp;
|
||||
|
||||
impl Ohttp {
|
||||
/// The key for this type.
|
||||
const KEY: SvcParamKey = SvcParamKey::OHTTP;
|
||||
}
|
||||
|
||||
impl Ohttp {
|
||||
/// Parses a ohttp value from its wire-format.
|
||||
pub fn parse<Src: Octets + ?Sized>(
|
||||
_parser: &mut Parser<'_, Src>,
|
||||
) -> Result<Self, ParseError> {
|
||||
Ok(Self)
|
||||
}
|
||||
}
|
||||
|
||||
//--- SvcParamValue et al.
|
||||
|
||||
impl SvcParamValue for Ohttp {
|
||||
fn key(&self) -> SvcParamKey {
|
||||
Self::KEY
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Octs: Octets + ?Sized> ParseSvcParamValue<'a, Octs> for Ohttp {
|
||||
fn parse_value(
|
||||
key: SvcParamKey,
|
||||
parser: &mut Parser<'a, Octs>,
|
||||
) -> Result<Option<Self>, ParseError> {
|
||||
if key == Self::KEY {
|
||||
Self::parse(parser).map(Some)
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<SrcOcts, Octs> ScanSvcParamValue<SrcOcts, Octs> for Ohttp
|
||||
where
|
||||
Octs: AsRef<[u8]>,
|
||||
SrcOcts: AsRef<[u8]> + ?Sized,
|
||||
{
|
||||
fn value_from_scan_octets<S: Scanner<Octets = Octs>>(
|
||||
_scanner: &mut S,
|
||||
key: SvcParamKey,
|
||||
octs: &SrcOcts,
|
||||
) -> Result<Option<Self>, S::Error> {
|
||||
if key == Self::KEY {
|
||||
if !octs.as_ref().is_empty() {
|
||||
return Err(S::Error::custom("ohttp takes no values"));
|
||||
}
|
||||
Ok(Some(Self))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ComposeSvcParamValue for Ohttp {
|
||||
fn compose_len(&self) -> u16 {
|
||||
0
|
||||
}
|
||||
|
||||
fn compose_value<Target: OctetsBuilder + ?Sized>(
|
||||
&self,
|
||||
_target: &mut Target,
|
||||
) -> Result<(), Target::AppendError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
//--- Display
|
||||
|
||||
impl fmt::Display for Ohttp {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("ohttp")
|
||||
}
|
||||
}
|
||||
|
||||
//--- Extend SvcParams and SvcParamsBuilder
|
||||
|
||||
impl<Octs: Octets + ?Sized> SvcParams<Octs> {
|
||||
/// Returns whether the [`Ohttp`] value is present.
|
||||
pub fn ohttp(&self) -> bool {
|
||||
self.first::<Ohttp>().is_some()
|
||||
}
|
||||
}
|
||||
|
||||
impl<Octs: OctetsBuilder + AsRef<[u8]> + AsMut<[u8]>> SvcParamsBuilder<Octs> {
|
||||
/// Adds the [`Ohttp`] value.
|
||||
pub fn ohttp(&mut self) -> Result<(), PushError> {
|
||||
self.push(&Ohttp)
|
||||
}
|
||||
}
|
||||
|
||||
//------------ TlsSupportedGroups -------------------------------------------------
|
||||
|
||||
octets_wrapper!(
|
||||
/// The ‘tls-supported-groups’ service parameter value.
|
||||
///
|
||||
/// This value is used to specify the endpoint's TLS supported group preferences.
|
||||
///
|
||||
/// This value type is described as part of the specification for
|
||||
/// TLS Key Share Prediction, currently
|
||||
/// [draft-ietf-tls-key-share-prediction](https://datatracker.ietf.org/doc/draft-ietf-tls-key-share-prediction).
|
||||
///
|
||||
/// A value of this type wraps an octets sequence that contains the
|
||||
/// integer values of the TLS supported groups preferences in network byte
|
||||
/// order. You can create a value of this type by providing an iterator
|
||||
/// over the keys to be included to the [`from_keys`][Self::from_keys]
|
||||
/// function. You can get an iterator over the keys in an existing value
|
||||
/// through the [`iter`][Self::iter] method.
|
||||
TlsSupportedGroups => TLS_SUPPORTED_GROUPS,
|
||||
TlsSupportedGroupsIter
|
||||
);
|
||||
|
||||
impl<Octs: AsRef<[u8]>> TlsSupportedGroups<Octs> {
|
||||
/// Creates a new tls-supported-groups value from an octets sequence.
|
||||
///
|
||||
/// The function checks that the octets sequence contains a properly
|
||||
/// encoded value of at most 65,535 octets. It does not check whether
|
||||
/// there are any duplicates in the data.
|
||||
pub fn from_octets(octets: Octs) -> Result<Self, ParseError> {
|
||||
TlsSupportedGroups::check_slice(octets.as_ref())?;
|
||||
Ok(unsafe { Self::from_octets_unchecked(octets) })
|
||||
}
|
||||
}
|
||||
|
||||
impl TlsSupportedGroups<[u8]> {
|
||||
/// Creates a new tls-supported-groups value from an octets slice.
|
||||
///
|
||||
/// The function checks that the octets slice contains a properly
|
||||
/// encoded value of at most 65,535 octets. It does not check whether
|
||||
/// there are any duplicates in the data.
|
||||
pub fn from_slice(slice: &[u8]) -> Result<&Self, ParseError> {
|
||||
Self::check_slice(slice)?;
|
||||
Ok(unsafe { Self::from_slice_unchecked(slice) })
|
||||
}
|
||||
|
||||
/// Checks that a slice contains a properly encoded tls-supported-groups value.
|
||||
fn check_slice(slice: &[u8]) -> Result<(), ParseError> {
|
||||
LongSvcParam::check_len(slice.len())?;
|
||||
if slice.is_empty()
|
||||
|| slice.len() % usize::from(u16::COMPOSE_LEN) != 0
|
||||
{
|
||||
return Err(ParseError::form_error(
|
||||
"invalid tls-supported-groups parameter",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<Octs: AsRef<[u8]>> TlsSupportedGroups<Octs> {
|
||||
/// Creates a new value from a list of keys.
|
||||
///
|
||||
/// The created value will contain all the keys returned by the iterator
|
||||
/// in the order provided. The function does not check for duplicates.
|
||||
///
|
||||
/// Returns an error if the octets builder runs out of space or the
|
||||
/// resulting value would be longer than 65,535 octets.
|
||||
pub fn from_keys(
|
||||
keys: impl Iterator<Item = u16>,
|
||||
) -> Result<Self, BuildValueError>
|
||||
where
|
||||
Octs: FromBuilder,
|
||||
<Octs as FromBuilder>::Builder: EmptyBuilder,
|
||||
{
|
||||
let mut octets = EmptyBuilder::empty();
|
||||
for item in keys {
|
||||
item.compose(&mut octets)?;
|
||||
}
|
||||
let octets = Octs::from_builder(octets);
|
||||
if LongSvcParam::check_len(octets.as_ref().len()).is_err() {
|
||||
return Err(BuildValueError::LongSvcParam);
|
||||
}
|
||||
Ok(unsafe { Self::from_octets_unchecked(octets) })
|
||||
}
|
||||
}
|
||||
|
||||
impl<Octs: AsRef<[u8]>> TlsSupportedGroups<Octs> {
|
||||
/// Parses a tls-supported-groups value from its wire format.
|
||||
pub fn parse<'a, Src: Octets<Range<'a> = Octs> + ?Sized>(
|
||||
parser: &mut Parser<'a, Src>,
|
||||
) -> Result<Self, ParseError> {
|
||||
Self::from_octets(parser.parse_octets(parser.remaining())?)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl<SrcOcts, Octs> ScanSvcParamValue<SrcOcts, Octs>
|
||||
for TlsSupportedGroups<Octs>
|
||||
where
|
||||
Octs: AsRef<[u8]>,
|
||||
SrcOcts: AsRef<[u8]> + ?Sized,
|
||||
{
|
||||
fn value_from_scan_octets<S: Scanner<Octets = Octs>>(
|
||||
scanner: &mut S,
|
||||
key: SvcParamKey,
|
||||
octs: &SrcOcts,
|
||||
) -> Result<Option<Self>, S::Error> {
|
||||
if key == TlsSupportedGroups::KEY {
|
||||
let mut tmp = scanner.octets_builder()?;
|
||||
let mut iter = SvcParamValueScanIter::from_slice(octs.as_ref());
|
||||
// keys must not be duplicated
|
||||
let mut keys = HashSet::<u16>::new();
|
||||
while let Some(item) = iter.next_no_escapes().map_err(|_| {
|
||||
S::Error::custom(
|
||||
"no escape sequences allowed in tls-supported-groups",
|
||||
)
|
||||
})? {
|
||||
let k = str::from_utf8(item)
|
||||
.map_err(|_| {
|
||||
S::Error::custom(
|
||||
"invalid key listed in SvcParamKey tls-supported-groups, must contain UTF-8 encoded numbers",
|
||||
)
|
||||
})?
|
||||
.parse::<u16>()
|
||||
.map_err(|_| {
|
||||
S::Error::custom(
|
||||
"invalid key listed in SvcParamKey tls-supported-groups, must contain positive 16-bit integers",
|
||||
)
|
||||
})?;
|
||||
if !keys.insert(k) {
|
||||
return Err(S::Error::custom(
|
||||
"tls-supported-groups contains duplicate values",
|
||||
));
|
||||
}
|
||||
k.compose(&mut tmp).map_err(|_| S::Error::short_buf())?;
|
||||
}
|
||||
Ok(Some(Self::from_octets(tmp.freeze()).map_err(|_| {
|
||||
S::Error::custom(
|
||||
"invalid svc param value for tls-supported-groups",
|
||||
)
|
||||
})?))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//--- Iterator
|
||||
|
||||
impl<Octs: AsRef<[u8]> + ?Sized> Iterator
|
||||
for TlsSupportedGroupsIter<'_, Octs>
|
||||
{
|
||||
type Item = u16;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.parser.remaining() == 0 {
|
||||
return None;
|
||||
}
|
||||
Some(
|
||||
u16::parse(&mut self.parser)
|
||||
.expect("invalid tls-supported-groups parameter"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
//--- Display
|
||||
|
||||
impl<Octs: Octets + ?Sized> fmt::Display for TlsSupportedGroups<Octs> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
for (i, v) in self.iter().enumerate() {
|
||||
if i == 0 {
|
||||
write!(f, "tls-supported-groups={}", v)?;
|
||||
} else {
|
||||
write!(f, ",{}", v)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
//--- Extend SvcParams and SvcParamsBuilder
|
||||
|
||||
impl<Octs: Octets + ?Sized> SvcParams<Octs> {
|
||||
/// Returns the content of the ‘tls-supported-groups’ value if present.
|
||||
pub fn tls_supported_groups(
|
||||
&self,
|
||||
) -> Option<TlsSupportedGroups<Octs::Range<'_>>> {
|
||||
self.first()
|
||||
}
|
||||
}
|
||||
|
||||
impl<Octs: OctetsBuilder + AsRef<[u8]> + AsMut<[u8]>> SvcParamsBuilder<Octs> {
|
||||
/// Adds a ‘tls-supported-groups’ value with the given keys.
|
||||
///
|
||||
/// Returns an error if there already is a ‘tls-supported-groups’ value, `keys`
|
||||
/// contains more values than fit into a service binding parameter value,
|
||||
/// or the underlying octets builder runs out of space.
|
||||
pub fn tls_supported_groups(
|
||||
&mut self,
|
||||
keys: impl AsRef<[SvcParamKey]>,
|
||||
) -> Result<(), PushValueError> {
|
||||
self.push_raw(
|
||||
TlsSupportedGroups::KEY,
|
||||
u16::try_from(
|
||||
keys.as_ref().len() * usize::from(SvcParamKey::COMPOSE_LEN),
|
||||
)
|
||||
.map_err(|_| PushValueError::LongSvcParam)?,
|
||||
|octs| {
|
||||
keys.as_ref().iter().try_for_each(|item| item.compose(octs))
|
||||
},
|
||||
)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
//------------ SvcParamValueScanIter -----------------------------------------
|
||||
|
||||
/// An iterator over the items of a comma separated list of octets, as used
|
||||
/// in SvcParamValue [RFC9460 A.1.] with escape sequences allowed
|
||||
///
|
||||
/// [RFC9460 A.1.]: https://www.rfc-editor.org/rfc/rfc9460.html#name-decoding-a-comma-separated-
|
||||
struct SvcParamValueScanIter<'a> {
|
||||
data: &'a [u8],
|
||||
start_next: usize,
|
||||
end: usize,
|
||||
}
|
||||
|
||||
impl<'a> SvcParamValueScanIter<'a> {
|
||||
pub fn from_slice(octs: &'a [u8]) -> Self {
|
||||
Self {
|
||||
data: octs,
|
||||
start_next: 0,
|
||||
end: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// Evaluating the escape sequences for SvcParamValues was broken and is
|
||||
// therefore removed for now. The needed fix is to move the escaped byte
|
||||
// to the left in the buffer and overwrite the backslash that was used to
|
||||
// escape said byte. So, r"\\" needs to turn into r"\" and r"\," into
|
||||
// r",".
|
||||
// https://www.rfc-editor.org/rfc/rfc9460.html#appendix-A.1-3
|
||||
// item = 1*OCTET
|
||||
// escaped-item = 1*(item-allowed / "\," / "\\")
|
||||
// comma-separated = [escaped-item *("," escaped-item)]
|
||||
|
||||
// pub fn next(&mut self) -> Option<&[u8]> {
|
||||
// if self.start_next >= self.data.len() {
|
||||
// None
|
||||
// } else {
|
||||
// let mut is_escaped = false;
|
||||
// let start = self.start_next;
|
||||
// self.end = self.start_next;
|
||||
|
||||
// loop {
|
||||
// if self.end < self.data.len() {
|
||||
// // Still data to read
|
||||
// if !is_escaped && self.data[self.end] == b',' {
|
||||
// // End of value. Return item
|
||||
// break;
|
||||
// } else {
|
||||
// // Value not yet ended. Read more
|
||||
// if is_escaped {
|
||||
// // TODO: move current byte one byte to the
|
||||
// // left to overwrite the escaping backslash
|
||||
// is_escaped = false;
|
||||
// } else if self.data[self.end] == b'\\' {
|
||||
// is_escaped = true;
|
||||
// }
|
||||
|
||||
// self.end += 1;
|
||||
// }
|
||||
// } else {
|
||||
// // End of data. Return item
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
|
||||
// self.start_next = self.end + 1;
|
||||
// Some(&self.data[start..self.end])
|
||||
// }
|
||||
// }
|
||||
|
||||
/// An iterator but error on escape sequences
|
||||
pub fn next_no_escapes(&mut self) -> Result<Option<&[u8]>, ()> {
|
||||
if self.start_next >= self.data.len() {
|
||||
Ok(None)
|
||||
} else {
|
||||
let start = self.start_next;
|
||||
self.end = self.start_next;
|
||||
|
||||
loop {
|
||||
if self.end < self.data.len() {
|
||||
// Still data to read
|
||||
if self.data[self.end] == b',' {
|
||||
// End of value. Return item
|
||||
break;
|
||||
} else {
|
||||
// Value not yet ended. Read more
|
||||
if self.data[self.end] == b'\\' {
|
||||
// Escaping not allowed
|
||||
return Err(());
|
||||
}
|
||||
self.end += 1;
|
||||
}
|
||||
} else {
|
||||
// End of data. Return item
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
self.start_next = self.end + 1;
|
||||
Ok(Some(&self.data[start..self.end]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//============ BuildValueError ===============================================
|
||||
|
||||
//------------ BuildValueError -----------------------------------------------
|
||||
|
||||
@@ -0,0 +1,387 @@
|
||||
//! TLSA record data.
|
||||
//!
|
||||
//! The TLSA Resource Record is used to associate a TLS server certificate or
|
||||
//! public key with the domain name of the RR
|
||||
//!
|
||||
//! [RFC 6698]: https://tools.ietf.org/html/rfc6698
|
||||
|
||||
// Currently a false positive on Tlsa. We cannot apply it there because
|
||||
// the allow attribute doesn't get copied to the code generated by serde.
|
||||
#![allow(clippy::needless_maybe_sized)]
|
||||
|
||||
use crate::base::cmp::CanonicalOrd;
|
||||
use crate::base::iana::{
|
||||
Rtype, TlsaCertificateUsage, TlsaMatchingType, TlsaSelector,
|
||||
};
|
||||
use crate::base::rdata::{ComposeRecordData, RecordData};
|
||||
use crate::base::scan::Scanner;
|
||||
use crate::base::wire::{Composer, ParseError};
|
||||
use crate::base::zonefile_fmt::{self, Formatter, ZonefileFmt};
|
||||
use crate::utils::base16;
|
||||
use core::cmp::Ordering;
|
||||
use core::{fmt, hash};
|
||||
use octseq::octets::{Octets, OctetsFrom, OctetsInto};
|
||||
use octseq::parse::Parser;
|
||||
|
||||
/// The TLSA Resource Record is used to associate a TLS server certificate or
|
||||
/// public key with the domain name of the RR
|
||||
#[derive(Clone)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Tlsa<Octs: ?Sized> {
|
||||
usage: TlsaCertificateUsage,
|
||||
selector: TlsaSelector,
|
||||
matching_type: TlsaMatchingType,
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
serde(
|
||||
serialize_with = "octseq::serde::SerializeOctets::serialize_octets",
|
||||
deserialize_with = "octseq::serde::DeserializeOctets::deserialize_octets",
|
||||
bound(
|
||||
serialize = "Octs: octseq::serde::SerializeOctets",
|
||||
deserialize = "Octs: octseq::serde::DeserializeOctets<'de>",
|
||||
)
|
||||
)
|
||||
)]
|
||||
/// Certificate Association Data
|
||||
data: Octs,
|
||||
}
|
||||
|
||||
impl Tlsa<()> {
|
||||
/// The rtype of this record data type.
|
||||
pub(crate) const RTYPE: Rtype = Rtype::TLSA;
|
||||
}
|
||||
|
||||
impl<Octs> Tlsa<Octs> {
|
||||
/// Create a Tlsa record data from provided parameters.
|
||||
pub fn new(
|
||||
usage: TlsaCertificateUsage,
|
||||
selector: TlsaSelector,
|
||||
matching_type: TlsaMatchingType,
|
||||
data: Octs,
|
||||
) -> Self {
|
||||
Self {
|
||||
usage,
|
||||
selector,
|
||||
matching_type,
|
||||
data,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the usage field.
|
||||
pub fn usage(&self) -> TlsaCertificateUsage {
|
||||
self.usage
|
||||
}
|
||||
|
||||
/// Get the selector field.
|
||||
pub fn selector(&self) -> TlsaSelector {
|
||||
self.selector
|
||||
}
|
||||
|
||||
/// Get the hash matching_type field.
|
||||
pub fn matching_type(&self) -> TlsaMatchingType {
|
||||
self.matching_type
|
||||
}
|
||||
|
||||
/// Get the certificate association data field.
|
||||
pub fn data(&self) -> &Octs {
|
||||
&self.data
|
||||
}
|
||||
|
||||
/// Parse the record data from wire format.
|
||||
pub fn parse<'a, Src: Octets<Range<'a> = Octs> + ?Sized>(
|
||||
parser: &mut Parser<'a, Src>,
|
||||
) -> Result<Self, ParseError> {
|
||||
let usage = TlsaCertificateUsage::parse(parser)?;
|
||||
let selector = TlsaSelector::parse(parser)?;
|
||||
let matching_type = TlsaMatchingType::parse(parser)?;
|
||||
let len = parser.remaining();
|
||||
let data = parser.parse_octets(len)?;
|
||||
Ok(Self {
|
||||
usage,
|
||||
selector,
|
||||
matching_type,
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse the record data from zonefile format.
|
||||
pub fn scan<S: Scanner<Octets = Octs>>(
|
||||
scanner: &mut S,
|
||||
) -> Result<Self, S::Error> {
|
||||
let usage = TlsaCertificateUsage::scan(scanner)?;
|
||||
let selector = TlsaSelector::scan(scanner)?;
|
||||
let matching_type = TlsaMatchingType::scan(scanner)?;
|
||||
let data = scanner.convert_entry(base16::SymbolConverter::new())?;
|
||||
|
||||
Ok(Self {
|
||||
usage,
|
||||
selector,
|
||||
matching_type,
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn flatten<Target: OctetsFrom<Octs>>(
|
||||
self,
|
||||
) -> Result<Tlsa<Target>, Target::Error> {
|
||||
self.convert_octets()
|
||||
}
|
||||
|
||||
pub(super) fn convert_octets<Target: OctetsFrom<Octs>>(
|
||||
self,
|
||||
) -> Result<Tlsa<Target>, Target::Error> {
|
||||
let Tlsa {
|
||||
usage,
|
||||
selector,
|
||||
matching_type,
|
||||
data,
|
||||
} = self;
|
||||
|
||||
Ok(Tlsa {
|
||||
usage,
|
||||
selector,
|
||||
matching_type,
|
||||
data: data.try_octets_into()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<Octs> RecordData for Tlsa<Octs> {
|
||||
fn rtype(&self) -> Rtype {
|
||||
Tlsa::RTYPE
|
||||
}
|
||||
}
|
||||
|
||||
impl<Octs: AsRef<[u8]>> ComposeRecordData for Tlsa<Octs> {
|
||||
fn rdlen(&self, _compress: bool) -> Option<u16> {
|
||||
Some(
|
||||
// usage + selector + matching_type + data_len
|
||||
u16::try_from(1 + 1 + 1 + self.data.as_ref().len())
|
||||
.expect("long TLSA rdata"),
|
||||
)
|
||||
}
|
||||
|
||||
fn compose_rdata<Target: Composer + ?Sized>(
|
||||
&self,
|
||||
target: &mut Target,
|
||||
) -> Result<(), Target::AppendError> {
|
||||
target.append_slice(&[self.usage.into()])?;
|
||||
target.append_slice(&[self.selector.into()])?;
|
||||
target.append_slice(&[self.matching_type.into()])?;
|
||||
target.append_slice(self.data.as_ref())
|
||||
}
|
||||
|
||||
fn compose_canonical_rdata<Target: Composer + ?Sized>(
|
||||
&self,
|
||||
target: &mut Target,
|
||||
) -> Result<(), Target::AppendError> {
|
||||
self.compose_rdata(target)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Octs: AsRef<[u8]>> hash::Hash for Tlsa<Octs> {
|
||||
fn hash<H: hash::Hasher>(&self, state: &mut H) {
|
||||
self.usage.hash(state);
|
||||
self.selector.hash(state);
|
||||
self.matching_type.hash(state);
|
||||
self.data.as_ref().hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl<Octs, Other> PartialEq<Tlsa<Other>> for Tlsa<Octs>
|
||||
where
|
||||
Octs: AsRef<[u8]> + ?Sized,
|
||||
Other: AsRef<[u8]> + ?Sized,
|
||||
{
|
||||
fn eq(&self, other: &Tlsa<Other>) -> bool {
|
||||
self.usage.eq(&other.usage)
|
||||
&& self.selector.eq(&other.selector)
|
||||
&& self.matching_type.eq(&other.matching_type)
|
||||
&& self.data.as_ref().eq(other.data.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl<Octs: AsRef<[u8]> + ?Sized> Eq for Tlsa<Octs> {}
|
||||
|
||||
impl<Octs: AsRef<[u8]>> fmt::Display for Tlsa<Octs> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{} {} {} ( ",
|
||||
u8::from(self.usage),
|
||||
u8::from(self.selector),
|
||||
u8::from(self.matching_type)
|
||||
)?;
|
||||
base16::display(&self.data, f)?;
|
||||
write!(f, " )")
|
||||
}
|
||||
}
|
||||
|
||||
impl<Octs: AsRef<[u8]>> fmt::Debug for Tlsa<Octs> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("Tlsa(")?;
|
||||
fmt::Display::fmt(self, f)?;
|
||||
f.write_str(")")
|
||||
}
|
||||
}
|
||||
|
||||
impl<Octs: AsRef<[u8]>> ZonefileFmt for Tlsa<Octs> {
|
||||
fn fmt(&self, p: &mut impl Formatter) -> zonefile_fmt::Result {
|
||||
p.block(|p| {
|
||||
p.write_token(self.usage)?;
|
||||
p.write_show(self.selector)?;
|
||||
p.write_show(self.matching_type)?;
|
||||
p.write_token(base16::encode_display(&self.data))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<Octs, Other> PartialOrd<Tlsa<Other>> for Tlsa<Octs>
|
||||
where
|
||||
Octs: AsRef<[u8]>,
|
||||
Other: AsRef<[u8]>,
|
||||
{
|
||||
fn partial_cmp(&self, other: &Tlsa<Other>) -> Option<Ordering> {
|
||||
match self.usage.partial_cmp(&other.usage) {
|
||||
Some(Ordering::Equal) => {}
|
||||
other => return other,
|
||||
}
|
||||
match self.selector.partial_cmp(&other.selector) {
|
||||
Some(Ordering::Equal) => {}
|
||||
other => return other,
|
||||
}
|
||||
match self.matching_type.partial_cmp(&other.matching_type) {
|
||||
Some(Ordering::Equal) => {}
|
||||
other => return other,
|
||||
}
|
||||
self.data.as_ref().partial_cmp(other.data.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl<Octs, Other> CanonicalOrd<Tlsa<Other>> for Tlsa<Octs>
|
||||
where
|
||||
Octs: AsRef<[u8]>,
|
||||
Other: AsRef<[u8]>,
|
||||
{
|
||||
fn canonical_cmp(&self, other: &Tlsa<Other>) -> Ordering {
|
||||
match self.usage.cmp(&other.usage) {
|
||||
Ordering::Equal => {}
|
||||
other => return other,
|
||||
}
|
||||
match self.selector.cmp(&other.selector) {
|
||||
Ordering::Equal => {}
|
||||
other => return other,
|
||||
}
|
||||
match self.matching_type.cmp(&other.matching_type) {
|
||||
Ordering::Equal => {}
|
||||
other => return other,
|
||||
}
|
||||
self.data.as_ref().cmp(other.data.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl<Octs: AsRef<[u8]>> Ord for Tlsa<Octs> {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
match self.usage.cmp(&other.usage) {
|
||||
Ordering::Equal => {}
|
||||
other => return other,
|
||||
}
|
||||
match self.selector.cmp(&other.selector) {
|
||||
Ordering::Equal => {}
|
||||
other => return other,
|
||||
}
|
||||
match self.matching_type.cmp(&other.matching_type) {
|
||||
Ordering::Equal => {}
|
||||
other => return other,
|
||||
}
|
||||
self.data.as_ref().cmp(other.data.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(all(feature = "std", feature = "bytes"))]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::base::rdata::test::{
|
||||
test_compose_parse, test_rdlen, test_scan,
|
||||
};
|
||||
use crate::utils::base16::decode;
|
||||
use std::string::ToString;
|
||||
use std::vec::Vec;
|
||||
|
||||
#[test]
|
||||
// allow redundant_closure because because of lifetime shenanigans
|
||||
// in test_compose_parse(Tlsa::parse), "FnOnce is not general enough"
|
||||
#[allow(clippy::redundant_closure)]
|
||||
fn tlsa_compose_parse_scan() {
|
||||
let usage = 0.into();
|
||||
let selector = 0.into();
|
||||
let matching_type = 1.into();
|
||||
let data_str = "d2abde240d7cd3ee6b4b28c54df034b97983a1d16e8a410e4561cb106618e971";
|
||||
let data: Vec<u8> = decode(data_str).unwrap();
|
||||
let rdata = Tlsa::new(usage, selector, matching_type, data);
|
||||
test_rdlen(&rdata);
|
||||
test_compose_parse(&rdata, |parser| Tlsa::parse(parser));
|
||||
test_scan(
|
||||
&[
|
||||
&u8::from(usage).to_string(),
|
||||
&u8::from(selector).to_string(),
|
||||
&u8::from(matching_type).to_string(),
|
||||
data_str,
|
||||
],
|
||||
Tlsa::scan,
|
||||
&rdata,
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "zonefile")]
|
||||
#[test]
|
||||
fn tlsa_parse_zonefile() {
|
||||
use crate::base::iana::{
|
||||
TlsaCertificateUsage, TlsaMatchingType, TlsaSelector,
|
||||
};
|
||||
use crate::base::Name;
|
||||
use crate::rdata::ZoneRecordData;
|
||||
use crate::zonefile::inplace::{Entry, Zonefile};
|
||||
|
||||
// section A.1
|
||||
let content = r#"
|
||||
example. 86400 IN SOA ns1 admin 2018031900 (
|
||||
1800 900 604800 86400 )
|
||||
86400 IN NS ns1
|
||||
86400 IN NS ns2
|
||||
86400 IN TLSA 0 0 1 (
|
||||
d2abde240d7cd3ee6b4b28c54df034b9
|
||||
7983a1d16e8a410e4561cb106618e971 )
|
||||
ns1 3600 IN A 203.0.113.63
|
||||
ns2 3600 IN AAAA 2001:db8::63
|
||||
"#;
|
||||
|
||||
let mut zone = Zonefile::load(&mut content.as_bytes()).unwrap();
|
||||
zone.set_origin(Name::root());
|
||||
while let Some(entry) = zone.next_entry().unwrap() {
|
||||
match entry {
|
||||
Entry::Record(record) => {
|
||||
if record.rtype() != Rtype::TLSA {
|
||||
continue;
|
||||
}
|
||||
match record.into_data() {
|
||||
ZoneRecordData::Tlsa(rd) => {
|
||||
assert_eq!(
|
||||
TlsaCertificateUsage::PKIX_TA,
|
||||
rd.usage()
|
||||
);
|
||||
assert_eq!(TlsaSelector::CERT, rd.selector());
|
||||
assert_eq!(
|
||||
TlsaMatchingType::SHA2_256,
|
||||
rd.matching_type()
|
||||
);
|
||||
}
|
||||
_ => panic!(),
|
||||
}
|
||||
}
|
||||
_ => panic!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -664,6 +664,63 @@ impl Scanner for EntryScanner<'_> {
|
||||
Ok(self.zonefile.buf.split_to(write).freeze())
|
||||
}
|
||||
|
||||
/// SVCB's SvcParams format can contain quoted SvcParamValues, therefore we need to concatenate
|
||||
/// multiple tokens into a single octet sequence if they appear without whitespace, e.g.: `SVCB
|
||||
/// 10 . key1="quoted value"` would normally parsed into the tokens `SVCB` `10` `.` `key1=` and
|
||||
/// `quoted value`, we need the last token to be `key1=quoted value`.
|
||||
fn scan_svcb_octets(&mut self) -> Result<Self::Octets, Self::Error> {
|
||||
self.zonefile.buf.require_token()?;
|
||||
|
||||
// The result will never be longer than the encoded form, so we can
|
||||
// trim off everything to the left already.
|
||||
self.zonefile.buf.trim_to(self.zonefile.buf.start);
|
||||
|
||||
let mut write;
|
||||
// Remember if we are inside a quoted value. If so the opening quote
|
||||
// has already been skipped over, it is not part of the value.
|
||||
let is_quoted = self.zonefile.buf.cat == ItemCat::Quoted;
|
||||
|
||||
// Skip over symbols that don’t need converting at the beginning.
|
||||
while self.zonefile.buf.next_ascii_symbol()?.is_some() {}
|
||||
|
||||
if self.zonefile.buf.cat == ItemCat::None {
|
||||
// The item has ended. Remove the double quote.
|
||||
let write = if is_quoted {
|
||||
self.zonefile.buf.start - 1
|
||||
} else {
|
||||
self.zonefile.buf.start
|
||||
};
|
||||
self.zonefile.buf.next_item()?;
|
||||
return Ok(self.zonefile.buf.split_to(write).freeze());
|
||||
}
|
||||
|
||||
// If we aren’t done yet, we have escaped characters to replace.
|
||||
write = self.zonefile.buf.start;
|
||||
|
||||
while let Some(sym) = self.zonefile.buf.next_symbol()? {
|
||||
self.zonefile.buf.buf[write] = sym.into_octet()?;
|
||||
write += 1;
|
||||
}
|
||||
|
||||
// Done. `write` marks the end.
|
||||
self.zonefile.buf.next_item()?;
|
||||
|
||||
// If the next token exists (i.e. is not None or LineFeed) and
|
||||
// directly follows this token without whitespace in between, it is
|
||||
// part of the current token/octet-string and we read further
|
||||
if !self.has_space() && self.zonefile.buf.cat == ItemCat::Quoted {
|
||||
while let Some(sym) = self.zonefile.buf.next_symbol()? {
|
||||
self.zonefile.buf.buf[write] = sym.into_octet()?;
|
||||
write += 1;
|
||||
}
|
||||
// Done. `write` marks the end.
|
||||
self.zonefile.buf.next_item()?;
|
||||
}
|
||||
|
||||
let x = self.zonefile.buf.split_to(write).freeze();
|
||||
Ok(x)
|
||||
}
|
||||
|
||||
fn scan_ascii_str<F, T>(&mut self, op: F) -> Result<T, Self::Error>
|
||||
where
|
||||
F: FnOnce(&str) -> Result<T, Self::Error>,
|
||||
|
||||
Reference in New Issue
Block a user