diff --git a/Cargo.toml b/Cargo.toml index 2018761..f18cd20 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,7 @@ domain = { git = "https://github.com/NLnetLabs/domain.git", branch = "main", fea "tokio-stream", "tsig", "unstable-client-transport", + "unstable-new", "unstable-sign", "unstable-validator", "unstable-zonetree" diff --git a/src/commands/readzone.rs b/src/commands/readzone.rs index 7baf592..3955e12 100644 --- a/src/commands/readzone.rs +++ b/src/commands/readzone.rs @@ -10,6 +10,7 @@ use std::path::{Path, PathBuf}; use bytes::BufMut; +use clap::Parser; use domain::base::name::FlattenInto; use domain::base::zonefile_fmt::ZonefileFmt; use domain::base::{CanonicalOrd, Record, ToName}; @@ -18,25 +19,77 @@ use domain::rdata::ZoneRecordData; use domain::zonefile::inplace::{self, Entry}; use domain::zonetree::types::StoredRecordData; use domain::zonetree::{StoredName, StoredRecord}; -use lexopt::Arg; use rayon::slice::ParallelSliceMut; use crate::env::{Env, Stream}; use crate::error::{Context, Error}; use crate::{Args, DISPLAY_KIND}; -use super::{parse_os, Command, LdnsCommand}; +use super::{Command, LdnsCommand}; //------------ Constants ----------------------------------------------------- //------------ ReadZone ------------------------------------------------------ -#[derive(Clone, Debug, clap::Args, PartialEq)] +#[derive(Clone, Debug, clap::Parser, PartialEq)] // TODO #[clap(after_help = "Read-Zone dnst HELP")] pub struct ReadZone { - /// Origin for the zone (REQUIRED). - #[arg(short = 'o', value_name = "domain", required = true)] + // ----------------------------------------------------------------------- + // Original ldns-read-zone options in ldns-read-zone -h order: + // ----------------------------------------------------------------------- + /// Canonicalize all resource records in the zone before printing + #[arg(short = 'c', default_value_t = false)] + canonicalize: bool, + + /// Only print DNSSEC data from the zone. This option skips every record + /// that is not of type NSEC, NSEC3, RRSIG or DNSKEY. DS records are not + /// printed. + #[arg(short = 'd', default_value_t = false)] + print_only_dnssec: bool, + + /// Print a (null) for the RRSIG inception, expiry and key data. This option + /// can be used when comparing different signing systems that use the same + /// DNSKEYs for signing but would have a slightly different timings/jitter. + #[arg(short = 'O', default_value_t = false)] + print_rrsig_null: bool, + + /// Do not print the SOA record + #[arg(short = 'n', default_value_t = false)] + print_not_soa: bool, + + /// Pad the SOA serial number with spaces so the number and the spaces + /// together take ten characters. This is useful for in file serial number + /// increments. + #[arg(short = 'p', default_value_t = false)] + pad_soa_serial: bool, + + /// Strip DNSSEC data from the zone. This option skips every record that is + /// of type NSEC, NSEC3, RRSIG or DNSKEY. DS records are still printed. + #[arg(short = 's', default_value_t = false)] + print_not_dnssec: bool, + + /// Set serial number to the given number, or when preceded by a sign, + /// offset the exisiting number with it. When giving the literal strings + /// YYYYMMDDxx or unixtime, the serial number is tried to be reset in + /// datecounter or in unixtime format respectively. Though is the updated + /// serial number is smaller than the original one, the original one is + /// simply increased by one. + #[arg(short = 'S', required = false)] + manipulate_serial: Option, + + // TODO: I don't think that -z implies -c. Because you can sort the records + // even though you print them in the original form i.e. with some capital + // letters. + /// Sort the zone before printing (this implies -c) + #[arg(short = 'z', default_value_t = false)] + canonical_sort: bool, + + // ----------------------------------------------------------------------- + // Extra options not supported by the original ldns-signzone: + // ----------------------------------------------------------------------- + /// Origin for the zone. + #[arg(short = 'o', value_name = "domain", required = false)] origin: Option, // ----------------------------------------------------------------------- @@ -55,8 +108,48 @@ pub struct ReadZone { } // TODO -const LDNS_HELP: &str = r###" -Read-Zone LDNS HELP +const LDNS_HELP: &str = r###"ldns-read-zone [OPTIONS] + Reads the zonefile and prints it. + The RR count of the zone is printed to stderr. + -0 zeroize timestamps and signature in RRSIG records. + -b include Bubble Babble encoding of DS's. + -c canonicalize all rrs in the zone. + -d only show DNSSEC data from the zone + -e + Do not print RRs of the given . + This option may be given multiple times. + -e is not meant to be used together with -E. + -E + Print only RRs of the given . + This option may be given multiple times. + -E is not meant to be used together with -e. + -h show this text + -n do not print the SOA record + -p prepend SOA serial with spaces so it takes exactly ten characters. + -s strip DNSSEC data from the zone + -S [[+|-] | YYYYMMDDxx | unixtime ] + Set serial number to or, when preceded by a sign, + offset the existing number with . With YYYYMMDDxx + the serial is formatted as a datecounter, and with unixtime as + the number of seconds since 1-1-1970. However, on serial + number decrease, +1 is used in stead. (implies -s) + -u + Mark for printing in unknown type format. + This option may be given multiple times. + -u is not meant to be used together with -U. + -U + Mark for not printing in unknown type format. + This option may be given multiple times. + The first occurrence of the -U option marks all RR types for + printing in unknown type format except for the given . + Subsequent -U options will clear the mark for those s + too, so that only the given s will be printed in the + presentation format specific for those s. + -U is not meant to be used together with -u. + -v shows the version and exits + -z sort the zone (implies -c). + +if no file is given standard input is read "###; impl LdnsCommand for ReadZone { @@ -64,50 +157,72 @@ impl LdnsCommand for ReadZone { const HELP: &'static str = LDNS_HELP; const COMPATIBLE_VERSION: &'static str = "1.8.4"; // TODO - fn parse_ldns>(args: I) -> Result { - let mut origin = Option::::None; - let mut zonefile = Option::::None; - - let mut parser = lexopt::Parser::from_args(args); - - while let Some(arg) = parser.next()? { - match arg { - Arg::Short('o') => { - let val = parser.value()?; - origin = Some(parse_os("-o", &val)?); - } - Arg::Value(val) => { - if zonefile.is_none() { - zonefile = Some(parse_os("zonefile", &val)?); - } - } - Arg::Short(x) => return Err(format!("Invalid short option: -{x}").into()), - Arg::Long(x) => { - return Err(format!("Long options are not supported, but `--{x}` given").into()) - } - } - } - - let Some(zonefile_path) = zonefile else { - return Err("Missing zonefile argument".into()); - }; + fn parse_ldns>(unargs: I) -> Result { + let args = ReadZone::parse(); + println!("{:?}", args); Ok(Args::from(Command::ReadZone(Self { - origin, - zonefile_path, - invoked_as_ldns: true, + canonicalize: args.canonicalize, + print_only_dnssec: args.print_only_dnssec, + print_rrsig_null: args.print_rrsig_null, + print_not_soa: args.print_not_soa, + pad_soa_serial: args.pad_soa_serial, + print_not_dnssec: args.print_not_dnssec, + manipulate_serial: args.manipulate_serial, + canonical_sort: args.canonical_sort, + origin: args.origin, + zonefile_path: args.zonefile_path, + invoked_as_ldns: args.invoked_as_ldns, }))) + // let mut parser = lexopt::Parser::from_args(args); + + // while let Some(arg) = parser.next()? { + // match arg { + // Arg::Short('o') => { + // let val = parser.value()?; + // origin = Some(parse_os("-o", &val)?); + // } + // Arg::Value(val) => { + // if zonefile.is_none() { + // zonefile = Some(parse_os("zonefile", &val)?); + // } + // } + // Arg::Short(x) => return Err(format!("Invalid short option: -{x}").into()), + // Arg::Long(x) => { + // return Err(format!("Long options are not supported, but `--{x}` given").into()) + // } + // } + // } + + // let Some(zonefile_path) = zonefile else { + // return Err("Missing zonefile argument".into()); + // }; + + // Ok(Args::from(Command::ReadZone(Self { + // origin, + // zonefile_path, + // invoked_as_ldns: true, + // }))) } } impl ReadZone { pub fn execute(self, env: impl Env) -> Result<(), Error> { // Read the zone file. - let records = self.load_zone(&env.in_cwd(&self.zonefile_path))?; + let records: SortedRecords< + domain::base::Name, + ZoneRecordData>, + MultiThreadedSorter, + > = self.load_zone(&env.in_cwd(&self.zonefile_path))?; let mut writer: FileOrStdout, _> = FileOrStdout::Stdout(env.stdout()); for rr in records.iter() { + if self.canonicalize { + // TODO: there is no way to modify the owner inplace? + let name: domain::base::Name = rr.owner().to_canonical_name(); + println!("{:?}", name); + } self.writeln_rr(&mut writer, rr)?; } @@ -257,9 +372,6 @@ impl domain::dnssec::sign::records::Sorter for MultiThreadedSorter { mod test { use std::str::FromStr; - use domain::base::CanonicalOrd; - use pretty_assertions::assert_eq; - use crate::commands::readzone::PathBuf; use crate::commands::readzone::StoredName; use crate::commands::Command; @@ -281,16 +393,21 @@ mod test { let cmd = FakeCmd::new(["dnst", "read-zone"]); let base = ReadZone { + canonicalize: false, + print_only_dnssec: false, + print_rrsig_null: false, + manipulate_serial: None, origin: Some(StoredName::from_str("example.org").unwrap()), + pad_soa_serial: false, + print_not_dnssec: false, + print_not_soa: false, + canonical_sort: false, zonefile_path: PathBuf::from("example.org.zone"), invoked_as_ldns: false, }; // Check the defaults - assert_eq!( - parse(cmd.args(["-oexample.org", "example.org.zone"])), - base - ); + assert_eq!(parse(cmd.args(["-oexample.org", "example.org.zone"])), base); } #[test] @@ -298,6 +415,7 @@ mod test { let res1 = FakeCmd::new([ "dnst", "read-zone", + "-c", "-o example.com.", "test-data/example.org", ]) @@ -310,12 +428,66 @@ mod test { #[test] fn name_canonicalization() { - use domain::base::name::Name; - let name1: Name = Name::::from_str("0.example.com.").unwrap(); - println!("{:?}", "0.example.com.".as_bytes()); - let name2: Name = Name::::from_str("1.example.com.").unwrap(); - println!("{:?}", "1.example.com.".as_bytes()); + use bytes::Bytes; - println!("{:?}", name1.canonical_cmp(&name2)); + use domain::base::iana::Class; + use domain::base::name::Name; + use domain::base::Record; + use domain::base::Ttl; + use domain::new; + use domain::rdata::Mx; + use domain::rdata::ZoneRecordData; + + let owner: Name = Name::::from_str("EXAMPLE.com.").unwrap(); + let class = Class::IN; + let ttl = Ttl::from_hours(1); + let exchange: Name = + Name::::from_str("mail.example.com.").unwrap(); + let data: ZoneRecordData> = ZoneRecordData::Mx(Mx::new(10, exchange)); + + let record: Record, ZoneRecordData>>; + record = Record::new(owner, class, ttl, data); + + let name: new::base::name::NameBuf = "EXAMPLE.com".parse().unwrap(); + let rtype: new::base::RType = new::base::RType::MX; + let rclass = new::base::RClass::IN; + let ttl = new::base::TTL::from(3600); + let preference = new::base::wire::U16::new(10); + let exchange: new::base::name::NameBuf = "MAIL.example.com".parse().unwrap(); + + let rdata: new::rdata::RecordData = + new::rdata::RecordData::Mx(new::rdata::Mx { + preference, + exchange, + }); + + let new_record = new::base::Record::new( + name, + rtype.clone(), + rclass.clone(), + ttl.clone(), + rdata.clone(), + ); + + let name: new::base::name::NameBuf = "example.com".parse().unwrap(); + + let other_new_record = new::base::Record::new(name, rtype, rclass, ttl, rdata); + + println!( + "{:?} {:?} {:?}", + new_record.rname, + new_record.rname.cmp(&other_new_record.rname), + other_new_record.rname + ); + + let mut target = Vec::::new(); + + record + .compose_canonical(&mut target) + .expect("This is fine!"); + + println!("{:?}", record); + + println!("{:?}", String::from_utf8(target)); } }