From 2e380af9bb99887fd4e146354505a42cdd85e071 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20B=C3=BChler?= Date: Thu, 21 Dec 2017 13:32:14 +0100 Subject: [PATCH] step --- Cargo.lock | 7 + lib/dnsbox-base/Cargo.toml | 4 + lib/dnsbox-base/src/common_types/binary.rs | 115 +++++++ .../src/common_types/binary/mod.rs | 90 ------ lib/dnsbox-base/src/common_types/classes.rs | 144 +++++++++ lib/dnsbox-base/src/common_types/mod.rs | 17 +- .../src/common_types/name/label.rs | 7 +- lib/dnsbox-base/src/common_types/name/mod.rs | 41 ++- .../src/common_types/name/name_mutations.rs | 3 +- .../{name_parser.rs => name_packet_parser.rs} | 17 +- .../src/common_types/name/name_text_parser.rs | 89 ++++++ lib/dnsbox-base/src/common_types/nsec.rs | 153 +++++++++ lib/dnsbox-base/src/common_types/nxt.rs | 94 ++++++ .../src/common_types/rr_type/mod.rs | 97 +++++- lib/dnsbox-base/src/common_types/text.rs | 115 +++++++ lib/dnsbox-base/src/common_types/text/mod.rs | 21 -- lib/dnsbox-base/src/common_types/time.rs | 38 +++ .../src/{records => common_types}/types.rs | 0 lib/dnsbox-base/src/common_types/uri.rs | 36 +++ lib/dnsbox-base/src/lib.rs | 3 + lib/dnsbox-base/src/records/mod.rs | 4 +- lib/dnsbox-base/src/records/registry.rs | 60 ++-- lib/dnsbox-base/src/records/structs.rs | 292 ++++++++++-------- lib/dnsbox-base/src/records/tests.rs | 116 ++++++- lib/dnsbox-base/src/records/unknown.rs | 47 +++ lib/dnsbox-base/src/records/weird_structs.rs | 153 +++++++++ lib/dnsbox-base/src/ser/mod.rs | 2 +- lib/dnsbox-base/src/ser/packet/mod.rs | 18 ++ lib/dnsbox-base/src/ser/rrdata.rs | 26 +- lib/dnsbox-base/src/ser/text/mod.rs | 114 ++++++- lib/dnsbox-base/src/ser/text/quoted.rs | 225 ++++++++++++++ lib/dnsbox-base/src/ser/text/std_impls.rs | 100 ++++++ lib/dnsbox-base/src/unsafe_ops/mod.rs | 11 + lib/dnsbox-derive/src/dns_packet_data.rs | 5 +- lib/dnsbox-derive/src/dns_text_data.rs | 56 ++++ lib/dnsbox-derive/src/lib.rs | 38 ++- lib/dnsbox-derive/src/rrdata.rs | 60 +++- 37 files changed, 2095 insertions(+), 323 deletions(-) create mode 100644 lib/dnsbox-base/src/common_types/binary.rs delete mode 100644 lib/dnsbox-base/src/common_types/binary/mod.rs create mode 100644 lib/dnsbox-base/src/common_types/classes.rs rename lib/dnsbox-base/src/common_types/name/{name_parser.rs => name_packet_parser.rs} (80%) create mode 100644 lib/dnsbox-base/src/common_types/name/name_text_parser.rs create mode 100644 lib/dnsbox-base/src/common_types/nsec.rs create mode 100644 lib/dnsbox-base/src/common_types/nxt.rs create mode 100644 lib/dnsbox-base/src/common_types/text.rs delete mode 100644 lib/dnsbox-base/src/common_types/text/mod.rs create mode 100644 lib/dnsbox-base/src/common_types/time.rs rename lib/dnsbox-base/src/{records => common_types}/types.rs (100%) create mode 100644 lib/dnsbox-base/src/common_types/uri.rs create mode 100644 lib/dnsbox-base/src/records/unknown.rs create mode 100644 lib/dnsbox-base/src/records/weird_structs.rs create mode 100644 lib/dnsbox-base/src/ser/text/quoted.rs create mode 100644 lib/dnsbox-base/src/ser/text/std_impls.rs create mode 100644 lib/dnsbox-base/src/unsafe_ops/mod.rs create mode 100644 lib/dnsbox-derive/src/dns_text_data.rs diff --git a/Cargo.lock b/Cargo.lock index ee83d18..df61858 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -45,6 +45,11 @@ name = "cfg-if" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "data-encoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "dbghelp-sys" version = "0.2.0" @@ -67,6 +72,7 @@ version = "0.1.0" dependencies = [ "byteorder 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "bytes 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", + "data-encoding 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "dnsbox-derive 0.1.0", "failure 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -198,6 +204,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum bytes 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "d828f97b58cc5de3e40c421d0cf2132d6b2da4ee0e11b8632fa838f0f9333ad6" "checksum cc 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7db2f146208d7e0fbee761b09cd65a7f51ccc38705d4e7262dad4d73b12a76b1" "checksum cfg-if 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d4c819a1287eb618df47cc647173c5c4c66ba19d888a6e50d605672aed3140de" +"checksum data-encoding 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "099d2591f809713931cd770f2bdf4b8a4d2eb7314bc762da4c375ecaa74af80f" "checksum dbghelp-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "97590ba53bcb8ac28279161ca943a924d1fd4a8fb3fa63302591647c4fc5b850" "checksum failure 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "934799b6c1de475a012a02dab0ace1ace43789ee4b99bcfbf1a2e3e8ced5de82" "checksum failure_derive 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c7cdda555bb90c9bb67a3b670a0f42de8e73f5981524123ad8578aafec8ddb8b" diff --git a/lib/dnsbox-base/Cargo.toml b/lib/dnsbox-base/Cargo.toml index e91aa32..036bd4d 100644 --- a/lib/dnsbox-base/Cargo.toml +++ b/lib/dnsbox-base/Cargo.toml @@ -11,3 +11,7 @@ failure = "0.1.1" lazy_static = "1.0.0" log = "0.3" smallvec = "0.4.4" +data-encoding = "2.1.0" + +[features] +no-unsafe = [] diff --git a/lib/dnsbox-base/src/common_types/binary.rs b/lib/dnsbox-base/src/common_types/binary.rs new file mode 100644 index 0000000..37394a0 --- /dev/null +++ b/lib/dnsbox-base/src/common_types/binary.rs @@ -0,0 +1,115 @@ +use bytes::Bytes; +use data_encoding::{self, HEXLOWER_PERMISSIVE}; +use errors::*; +use failure::{Fail, ResultExt}; +use ser::packet::{DnsPacketData, remaining_bytes, short_blob}; +use ser::text::*; +use std::fmt; +use std::io::Cursor; + +static WHITESPACE: &str = "\t\n\x0c\r "; // \f == \x0c formfeed + +lazy_static!{ + pub(crate) static ref HEXLOWER_PERMISSIVE_ALLOW_WS: data_encoding::Encoding = { + let mut spec = data_encoding::Specification::new(); + spec.symbols.push_str("0123456789abcdef"); + spec.translate.from.push_str("ABCDEF"); + spec.translate.to.push_str("abcdef"); + spec.ignore.push_str(WHITESPACE); + spec.encoding().unwrap() + }; + + static ref BASE64_ALLOW_WS: data_encoding::Encoding = { + let mut spec = data_encoding::Specification::new(); + spec.symbols.push_str("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"); + spec.padding = Some('='); + spec.ignore.push_str(WHITESPACE); + spec.encoding().unwrap() + }; +} + +// Similar to `ShortText`, but uses (unquoted, no spaces allowed) hex +// for text representation; "-" when empty +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct HexShortBlob(Bytes); + +impl DnsPacketData for HexShortBlob { + fn deserialize(data: &mut Cursor) -> Result { + Ok(HexShortBlob(short_blob(data)?)) + } +} + +impl DnsTextData for HexShortBlob { + fn dns_parse(data: &mut &str) -> ::errors::Result { + let s = next_field(data)?; + if s == "-" { + Ok(HexShortBlob(Bytes::new())) + } else { + let raw = HEXLOWER_PERMISSIVE.decode(s.as_bytes()) + .with_context(|e| e.context(format!("invalid hex: {:?}", s)))?; + ensure!(raw.len() < 256, "short hex field must be at most 255 bytes long"); + Ok(HexShortBlob(raw.into())) + } + } + + fn dns_format(&self, f: &mut DnsTextFormatter) -> fmt::Result { + if self.0.is_empty() { + write!(f, "-") + } else { + write!(f, "{}", HEXLOWER_PERMISSIVE.encode(&self.0)) + } + } +} + +// No length byte (or restriction), just all data to end of record. uses +// base64 encoding for text representation, whitespace allowed, padding +// required. +// +// No following field allowed, i.e. last field in the record. +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct Base64RemainingBlob(Bytes); + +impl DnsPacketData for Base64RemainingBlob { + fn deserialize(data: &mut Cursor) -> Result { + Ok(Base64RemainingBlob(remaining_bytes(data))) + } +} + +impl DnsTextData for Base64RemainingBlob { + fn dns_parse(data: &mut &str) -> ::errors::Result { + skip_whitespace(data); + let result = BASE64_ALLOW_WS.decode(data.as_bytes()) + .with_context(|e| e.context(format!("invalid base64: {:?}", data)))?; + *data = ""; + Ok(Base64RemainingBlob(result.into())) + } + + fn dns_format(&self, f: &mut DnsTextFormatter) -> fmt::Result { + write!(f, "{}", BASE64_ALLOW_WS.encode(&self.0)) + } +} + +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct HexRemainingBlob(Bytes); + +// No length byte, just all data to end of record. uses hex encoding for +// text representation (spaces are ignored - last field in record). +impl DnsPacketData for HexRemainingBlob { + fn deserialize(data: &mut Cursor) -> Result { + Ok(HexRemainingBlob(remaining_bytes(data))) + } +} + +impl DnsTextData for HexRemainingBlob { + fn dns_parse(data: &mut &str) -> ::errors::Result { + skip_whitespace(data); + let result = HEXLOWER_PERMISSIVE_ALLOW_WS.decode(data.as_bytes()) + .with_context(|e| e.context(format!("invalid hex: {:?}", data)))?; + *data = ""; + Ok(HexRemainingBlob(result.into())) + } + + fn dns_format(&self, f: &mut DnsTextFormatter) -> fmt::Result { + write!(f, "{}", HEXLOWER_PERMISSIVE_ALLOW_WS.encode(&self.0)) + } +} diff --git a/lib/dnsbox-base/src/common_types/binary/mod.rs b/lib/dnsbox-base/src/common_types/binary/mod.rs deleted file mode 100644 index e7242ef..0000000 --- a/lib/dnsbox-base/src/common_types/binary/mod.rs +++ /dev/null @@ -1,90 +0,0 @@ -use bytes::{Bytes, Buf}; -use std::io::Cursor; - -use ser::DnsPacketData; -use errors::*; - -#[derive(Clone, Debug)] -pub struct HexShortBlob(Bytes); - -// Similar to `ShortText`, but uses (unquoted, no spaces allowed) hex -// for text representation; "-" when empty -impl DnsPacketData for HexShortBlob { - fn deserialize(data: &mut Cursor) -> Result { - check_enough_data!(data, 1, "HexShortBlob length"); - let label_len = data.get_u8() as usize; - check_enough_data!(data, label_len, "HexShortBlob content"); - let pos = data.position() as usize; - let text = data.get_ref().slice(pos, pos + label_len); - data.advance(label_len); - Ok(HexShortBlob(text)) - } -} - -#[derive(Clone, Debug)] -pub struct Base32ShortBlob(Bytes); - -// Similar to `ShortText`, but uses (unquoted, no spaces allowed) base32 -// for text representation; "-" when empty -impl DnsPacketData for Base32ShortBlob { - fn deserialize(data: &mut Cursor) -> Result { - check_enough_data!(data, 1, "Base32ShortBlob length"); - let label_len = data.get_u8() as usize; - check_enough_data!(data, label_len, "Base32ShortBlob content"); - let pos = data.position() as usize; - let text = data.get_ref().slice(pos, pos + label_len); - data.advance(label_len); - Ok(Base32ShortBlob(text)) - } -} - -#[derive(Clone, Debug)] -pub struct LongText(Vec); - -// RFC 1035 names this `One or more `. No following -// field allowed. -impl DnsPacketData for LongText { - fn deserialize(data: &mut Cursor) -> Result { - let mut texts = Vec::new(); - loop { - check_enough_data!(data, 1, "LongText length"); - let label_len = data.get_u8() as usize; - check_enough_data!(data, label_len, "LongText content"); - let pos = data.position() as usize; - texts.push(data.get_ref().slice(pos, pos + label_len)); - data.advance(label_len); - if !data.has_remaining() { break; } - } - Ok(LongText(texts)) - } -} - -#[derive(Clone, Debug)] -pub struct RemainingText(Bytes); - -// No length byte, just all data to end of record. uses base64 encoding -// for text representation -impl DnsPacketData for RemainingText { - fn deserialize(data: &mut Cursor) -> Result { - let pos = data.position() as usize; - let len = data.remaining(); - let text = data.get_ref().slice(pos, pos + len); - data.advance(len); - Ok(RemainingText(text)) - } -} - -#[derive(Clone, Debug)] -pub struct HexRemainingBlob(Bytes); - -// No length byte, just all data to end of record. uses hex encoding for -// text representation (spaces are ignored - last field in record). -impl DnsPacketData for HexRemainingBlob { - fn deserialize(data: &mut Cursor) -> Result { - let pos = data.position() as usize; - let len = data.remaining(); - let text = data.get_ref().slice(pos, pos + len); - data.advance(len); - Ok(HexRemainingBlob(text)) - } -} diff --git a/lib/dnsbox-base/src/common_types/classes.rs b/lib/dnsbox-base/src/common_types/classes.rs new file mode 100644 index 0000000..3cce6c0 --- /dev/null +++ b/lib/dnsbox-base/src/common_types/classes.rs @@ -0,0 +1,144 @@ +use bytes::Bytes; +use errors::*; +use ser::DnsPacketData; +use ser::text::{DnsTextData, DnsTextFormatter, next_field}; +use std::fmt; +use std::io::Cursor; +use std::borrow::Cow; + +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +pub struct Class(pub u16); + +pub const IN : Class = Class(KnownClass::IN as u16); +pub const CH : Class = Class(KnownClass::CH as u16); +pub const HS : Class = Class(KnownClass::HS as u16); +pub const NONE : Class = Class(KnownQClass::NONE as u16); +pub const ANY : Class = Class(KnownQClass::ANY as u16); + +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +#[repr(u16)] +#[allow(non_camel_case_types)] +pub enum KnownClass { + // try to list "original" rfc + IN = 0x0001, // RFC 1035 + CH = 0x0003, // "Chaos" + HS = 0x0004, // "Hesiod" +} + +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +#[repr(u16)] +#[allow(non_camel_case_types)] +pub enum KnownQClass { + // try to list "original" rfc + NONE = 0x00fe, // RFC 2136 + ANY = 0x00ff, // RFC 1035: "*" +} + +impl Class { + pub fn name(self) -> Cow<'static, str> { + Cow::Borrowed(match self { + IN => "IN", + CH => "CH", + HS => "HS", + NONE => "NONE", + ANY => "ANY", + _ => return Cow::Owned(self.generic_name()), + }) + } + + /// uses generic name for QCLASS values (`is_qclass`) + pub fn class_name(self) -> Cow<'static, str> { + if self.is_qclass() { + Cow::Owned(self.generic_name()) + } else { + self.name() + } + } + + /// QCLASS names can overlap with (Q)TYPE names + /// + /// classes unknown to this implementation never count as QCLASS, + /// but they also are only represented using the generic "CLASS..." + /// names, which don't overlap with (Q)TYPE names. + pub fn is_qclass(self) -> bool { + match self { + NONE => true, + ANY => true, + _ => false, + } + } + + pub fn from_name(name: &str) -> Option { + use std::ascii::AsciiExt; + if let Some(n) = Self::class_from_name(name) { return Some(n); } + // explicit QCLASS names + if name.eq_ignore_ascii_case("NONE") { return Some(NONE); } + if name.eq_ignore_ascii_case("ANY") { return Some(ANY); } + None + } + + /// similar to `from_name`, but doesn't accept QCLASS names (it + /// always accepts "CLASS..." names though, even if they are known to + /// be of type QCLASS) + pub fn class_from_name(name: &str) -> Option { + use std::ascii::AsciiExt; + if name.eq_ignore_ascii_case("IN") { return Some(IN); } + if name.eq_ignore_ascii_case("CH") { return Some(CH); } + if name.eq_ignore_ascii_case("HS") { return Some(HS); } + if name.as_bytes()[0..5].eq_ignore_ascii_case(b"CLASS") { + if let Ok(c) = name[5..].parse::() { + return Some(Class(c)) + } + } + None + } + + pub fn write_class_name(self, f: &mut fmt::Formatter) -> fmt::Result { + if self.is_qclass() { + self.write_generic_name(f) + } else { + write!(f, "{}", self) + } + } + + pub fn write_generic_name(self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "CLASS{}", self.0) + } + + pub fn generic_name(self) -> String { + format!("CLASS{}", self.0) + } +} + +impl fmt::Display for Class { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let n = match *self { + IN => "IN", + CH => "CH", + HS => "HS", + NONE => "NONE", + ANY => "ANY", + _ => { + return self.write_generic_name(f) + }, + }; + write!(f, "{}", n) + } +} + +impl DnsPacketData for Class { + fn deserialize(data: &mut Cursor) -> Result { + Ok(Class(DnsPacketData::deserialize(data)?)) + } +} + +impl DnsTextData for Class { + fn dns_parse(data: &mut &str) -> Result { + let field = next_field(data)?; + Class::from_name(field).ok_or_else(|| format_err!("unknown class {:?}", field)) + } + + fn dns_format(&self, f: &mut DnsTextFormatter) -> fmt::Result { + write!(f, "{}", self) + } +} diff --git a/lib/dnsbox-base/src/common_types/mod.rs b/lib/dnsbox-base/src/common_types/mod.rs index 29b64e1..577fde8 100644 --- a/lib/dnsbox-base/src/common_types/mod.rs +++ b/lib/dnsbox-base/src/common_types/mod.rs @@ -1,9 +1,20 @@ pub mod name; pub mod text; pub mod binary; +pub mod classes; +pub mod types; +mod nsec; +mod nxt; mod rr_type; +mod time; +mod uri; -pub use self::name::{DnsName, DnsCompressedName}; -pub use self::text::{ShortText}; -pub use self::binary::{HexShortBlob, Base32ShortBlob, LongText, RemainingText, HexRemainingBlob}; +pub use self::binary::{HexShortBlob, Base64RemainingBlob, HexRemainingBlob}; +pub use self::name::{DnsName, DnsCanonicalName, DnsCompressedName}; +pub use self::nsec::{NsecTypeBitmap, NextHashedOwnerName}; pub use self::rr_type::Type; +pub use self::classes::Class; +pub use self::text::{ShortText, LongText, UnquotedShortText, RemainingText}; +pub use self::uri::UriText; +pub use self::time::Time; +pub use self::nxt::NxtTypeBitmap; diff --git a/lib/dnsbox-base/src/common_types/name/label.rs b/lib/dnsbox-base/src/common_types/name/label.rs index 66f7809..89f7030 100644 --- a/lib/dnsbox-base/src/common_types/name/label.rs +++ b/lib/dnsbox-base/src/common_types/name/label.rs @@ -214,26 +214,25 @@ impl<'a> fmt::Debug for DnsLabelRef<'a> { impl<'a> fmt::Display for DnsLabelRef<'a> { fn fmt(&self, w: &mut fmt::Formatter) -> fmt::Result { - use std::str; let mut done = 0; for pos in 0..self.label.len() { let c = self.label[pos]; if c <= 0x21 || c >= 0x7e || b'.' == c || b'\\' == c { // flush if done < pos { - w.write_str(unsafe {str::from_utf8_unchecked(&self.label[done..pos])})?; + w.write_str(::unsafe_ops::from_utf8_unchecked(&self.label[done..pos]))?; } match c { b'.' => w.write_str(r#"\."#)?, b'\\' => w.write_str(r#"\\"#)?, - _ => write!(w, r"\{:03o}", c)?, + _ => write!(w, r"\{:03}", c)?, } done = pos + 1; } } // final flush if done < self.label.len() { - w.write_str(unsafe {str::from_utf8_unchecked(&self.label[done..])})?; + w.write_str(::unsafe_ops::from_utf8_unchecked(&self.label[done..]))?; } Ok(()) } diff --git a/lib/dnsbox-base/src/common_types/name/mod.rs b/lib/dnsbox-base/src/common_types/name/mod.rs index 4d1168f..a34f881 100644 --- a/lib/dnsbox-base/src/common_types/name/mod.rs +++ b/lib/dnsbox-base/src/common_types/name/mod.rs @@ -15,12 +15,13 @@ pub use self::label::*; mod display; mod label; mod name_mutations; -mod name_parser; +mod name_packet_parser; +mod name_text_parser; #[derive(Clone,Copy,PartialEq,Eq,PartialOrd,Ord,Hash,Debug)] enum LabelOffset { LabelStart(u8), - PacketStart(u8), + PacketStart(u16), } // the heap meta data is usually at least 2*usize big; assuming 64-bit @@ -78,6 +79,15 @@ pub struct DnsName { total_len: u8, } +/// names that should be written in canonical form for DNSSEC according +/// to https://tools.ietf.org/html/rfc4034#section-6.2 +/// +/// TODO: make it a newtype. +/// +/// DnsCompressedName always needs to be written in canonical form for +/// DNSSEC. +pub type DnsCanonicalName = DnsName; + impl DnsName { /// Create new name representing the DNS root (".") pub fn new_root() -> Self { @@ -352,12 +362,12 @@ mod tests { ); check_uncompressed_display( b"\x07e!am.l\\\x03com\x00", - "e\\041am\\.l\\\\.com.", + "e\\033am\\.l\\\\.com.", 2, ); check_uncompressed_debug( b"\x07e!am.l\\\x03com\x00", - r#""e\\041am\\.l\\\\.com.""#, + r#""e\\033am\\.l\\\\.com.""#, ); } @@ -451,27 +461,38 @@ mod tests { ); } + #[test] + fn parse_invalid_compressed_name() { + de_compressed(b"\x11com\x00\x07example\xc0\x00", 5).unwrap_err(); + de_compressed(b"\x10com\x00\x07example\xc0\x00", 5).unwrap_err(); + } + #[test] fn parse_and_display_compressed_name() { check_compressed_display( - b"\x03com\x00\x07example\xc0", 5, + b"\x03com\x00\x07example\xc0\x00", 5, "example.com.", 2, ); check_compressed_display( - b"\x03com\x00\x07e!am.l\\\xc0", 5, - "e\\041am\\.l\\\\.com.", + b"\x03com\x00\x07e!am.l\\\xc0\x00", 5, + "e\\033am\\.l\\\\.com.", 2, ); check_compressed_debug( - b"\x03com\x00\x07e!am.l\\\xc0", 5, - r#""e\\041am\\.l\\\\.com.""#, + b"\x03com\x00\x07e!am.l\\\xc0\x00", 5, + r#""e\\033am\\.l\\\\.com.""#, + ); + check_compressed_display( + b"\x03com\x00\x07example\xc0\x00\x03www\xc0\x05", 15, + "www.example.com.", + 3, ); } #[test] fn modifications_compressed() { - let mut name = de_compressed(b"\x03com\x00\x07example\xc0", 5).unwrap(); + let mut name = de_compressed(b"\x03com\x00\x07example\xc0\x00\xc0\x05", 15).unwrap(); name.push_front(DnsLabelRef::new(b"www").unwrap()).unwrap(); assert_eq!( diff --git a/lib/dnsbox-base/src/common_types/name/name_mutations.rs b/lib/dnsbox-base/src/common_types/name/name_mutations.rs index a7e3bb2..e1e3948 100644 --- a/lib/dnsbox-base/src/common_types/name/name_mutations.rs +++ b/lib/dnsbox-base/src/common_types/name/name_mutations.rs @@ -141,7 +141,8 @@ impl DnsName { if label_offsets.is_empty() { // root name let mut data = data.unwrap_or_else(|_| BytesMut::with_capacity(new_len)); - data.put_u8(0); + unsafe { data.set_len(new_len); } + data[0] = 0; return (data, 0) } diff --git a/lib/dnsbox-base/src/common_types/name/name_parser.rs b/lib/dnsbox-base/src/common_types/name/name_packet_parser.rs similarity index 80% rename from lib/dnsbox-base/src/common_types/name/name_parser.rs rename to lib/dnsbox-base/src/common_types/name/name_packet_parser.rs index c78d7e8..95186ae 100644 --- a/lib/dnsbox-base/src/common_types/name/name_parser.rs +++ b/lib/dnsbox-base/src/common_types/name/name_packet_parser.rs @@ -5,7 +5,7 @@ impl DnsName { /// `data`: bytes of packet from beginning until at least the end of the name /// `start_pos`: position of first byte of the name /// `uncmpr_offsets`: offsets of uncompressed labels so far - /// `label_len`: first compressed label length (`0xc0 | offset`) + /// `label_len`: first compressed label length (`0xc0 | offset-high, offset-low`) /// `total_len`: length of (uncompressed) label encoding so far fn parse_name_compressed_cont(data: Bytes, start_pos: usize, uncmpr_offsets: SmallVec<[u8;16]>, mut total_len: usize, mut label_len: u8) -> Result { let mut label_offsets = uncmpr_offsets.into_iter() @@ -15,12 +15,14 @@ impl DnsName { let mut pos = start_pos + total_len; 'next_compressed: loop { { - let new_pos = (label_len & 0x3f) as usize; - if new_pos >= pos { bail!("Compressed label offset to big") } + ensure!(pos + 1 < data.len(), "not enough data for compressed label"); + let new_pos = ((label_len as usize & 0x3f) << 8) | (data[pos + 1] as usize); + ensure!(new_pos < pos, "Compressed label offset to big"); pos = new_pos; } loop { + ensure!(pos < data.len(), "not enough data for label"); label_len = data[pos]; if 0 == label_len { @@ -31,14 +33,14 @@ impl DnsName { }) } - if label_len & 0xc == 0xc { continue 'next_compressed; } - if label_len > 63 { bail!("Invalid label length {}", label_len) } + if label_len & 0xc0 == 0xc0 { continue 'next_compressed; } + ensure!(label_len < 64, "Invalid label length {}", label_len); total_len += 1 + label_len as usize; // max len 255, but there also needs to be an empty label at the end if total_len > 254 { bail!("DNS name too long") } - label_offsets.push(LabelOffset::PacketStart(pos as u8)); + label_offsets.push(LabelOffset::PacketStart(pos as u16)); pos += 1 + label_len as usize; } } @@ -63,6 +65,9 @@ impl DnsName { if label_len & 0xc0 == 0xc0 { // compressed label if !accept_compressed { bail!("Invalid label compression {}", label_len) } + check_enough_data!(data, 1, "DnsName compressed label target"); + // eat second part of compressed label + data.get_u8(); let end_pos = data.position() as usize; let data = data.get_ref().slice(0, end_pos); diff --git a/lib/dnsbox-base/src/common_types/name/name_text_parser.rs b/lib/dnsbox-base/src/common_types/name/name_text_parser.rs new file mode 100644 index 0000000..99b7b61 --- /dev/null +++ b/lib/dnsbox-base/src/common_types/name/name_text_parser.rs @@ -0,0 +1,89 @@ +use super::*; +use ser::text::{DnsTextData, DnsTextFormatter, next_field, quoted}; + +impl DnsName { + /// Parse text representation of a domain name + pub fn parse<'a, O>(value: &str, origin: O) -> ::errors::Result + where + O: IntoIterator> + { + let raw = value.as_bytes(); + let mut name = DnsName::new_root(); + if raw == b"." { + return Ok(name); + } else if raw == b"@" { + for l in origin.into_iter() { name.push_back(l)?; } + return Ok(name); + } + ensure!(!raw.is_empty(), "invalid empty name"); + let mut label = Vec::new(); + let mut pos = 0; + while pos < raw.len() { + if raw[pos] == b'.' { + ensure!(!label.is_empty(), "empty label in name: {:?}", value); + name.push_back(DnsLabelRef::new(&label)?)?; + label.clear(); + } else if raw[pos] == b'\\' { + ensure!(pos + 1 < raw.len(), "unexpected end of name after backslash: {:?}", value); + if raw[pos+1] >= b'0' && raw[pos+1] <= b'9' { + // \ddd escape + ensure!(pos + 3 < raw.len(), "unexpected end of name after backslash with digit: {:?}", value); + ensure!(raw[pos+2] >= b'0' && raw[pos+2] <= b'9' && raw[pos+3] >= b'0' && raw[pos+3] <= b'9', "expected three digits after backslash in name: {:?}", name); + let d1 = (raw[pos+1] - b'0') as u32; + let d2 = (raw[pos+2] - b'0') as u32; + let d3 = (raw[pos+3] - b'0') as u32; + let v = d1 * 100 + d2 * 10 + d3; + ensure!(v < 256, "invalid escape in name, {} > 255: {:?}", v, name); + label.push(v as u8); + } else { + ensure!(!quoted::is_ascii_whitespace(raw[pos+1]), "whitespace cannot be escaped with backslash prefix; encode it as \\{:03} in: {:?}", raw[pos+1], name); + label.push(raw[pos+1]); + } + } else { + ensure!(!quoted::is_ascii_whitespace(raw[pos+1]), "whitespace must be encoded as \\{:03} in: {:?}", raw[pos+1], name); + label.push(raw[pos]); + } + pos += 1; + } + + if !label.is_empty() { + // no trailing dot, relative name + name.push_back(DnsLabelRef::new(&label)?)?; + for l in origin.into_iter() { name.push_back(l)?; } + } + + Ok(name) + } +} + +impl DnsTextData for DnsName { + fn dns_parse(data: &mut &str) -> ::errors::Result { + let field = next_field(data)?; + DnsName::parse(field, &DnsName::new_root()) + } + + fn dns_format(&self, f: &mut DnsTextFormatter) -> fmt::Result { + write!(f, "{}", self) + } +} + +impl DnsCompressedName { + /// Parse text representation of a domain name + pub fn parse<'a, O>(value: &str, origin: O) -> ::errors::Result + where + O: IntoIterator> + { + Ok(DnsCompressedName(DnsName::parse(value, origin)?)) + } +} + +impl DnsTextData for DnsCompressedName { + fn dns_parse(data: &mut &str) -> ::errors::Result { + let field = next_field(data)?; + DnsCompressedName::parse(field, &DnsName::new_root()) + } + + fn dns_format(&self, f: &mut DnsTextFormatter) -> fmt::Result { + self.0.dns_format(f) + } +} diff --git a/lib/dnsbox-base/src/common_types/nsec.rs b/lib/dnsbox-base/src/common_types/nsec.rs new file mode 100644 index 0000000..4f922b4 --- /dev/null +++ b/lib/dnsbox-base/src/common_types/nsec.rs @@ -0,0 +1,153 @@ +use bytes::{Bytes, Buf}; +use common_types::Type; +use data_encoding; +use errors::*; +use failure::{Fail, ResultExt}; +use ser::packet::{DnsPacketData, remaining_bytes, short_blob}; +use ser::text::{DnsTextData, DnsTextFormatter, skip_whitespace, next_field}; +use std::collections::BTreeSet; +use std::fmt; +use std::io::Cursor; + +static WHITESPACE: &str = "\t\n\x0c\r "; // \f == \x0c formfeed + +lazy_static!{ + static ref BASE32HEX_NOPAD_ALLOW_WS: data_encoding::Encoding = { + let mut spec = data_encoding::Specification::new(); + spec.symbols.push_str("0123456789ABCDEFGHIJKLMNOPQRSTUV"); + spec.translate.from.push_str("abcdefghijklmnopqrstuv"); + spec.translate.to.push_str("ABCDEFGHIJKLMNOPQRSTUV"); + spec.ignore.push_str(WHITESPACE); + spec.encoding().unwrap() + }; +} + +/// Last field in a record! +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct NsecTypeBitmap { + raw: Bytes, + set: BTreeSet, +} + +impl NsecTypeBitmap { + pub fn from_set(set: BTreeSet) -> Self { + let mut raw = Vec::new(); + let mut current_window_base = None; + let mut window_pos = 0; + let mut window_len = 0; + for t in &set { + let base = (t.0 >> 8) as u8; + if current_window_base != Some(base) { + // new window + current_window_base = Some(base); + window_pos = raw.len(); + window_len = 0; + raw.push(base); + raw.push(0); + } + + let bit_ndx = t.0 as u8; + let byte_ndx = bit_ndx / 8; + if byte_ndx >= window_len { + // make window bigger, fill with zeroes + let new_window_len = byte_ndx + 1; + let new_raw_len = raw.len() + (new_window_len - window_len) as usize; + raw.resize(new_raw_len, 0); + raw[window_pos + 1] = new_window_len; + window_len = new_window_len; + } + + let mask = 0x80 >> (bit_ndx % 8); + raw[window_pos + 2 + byte_ndx as usize] |= mask; + } + + NsecTypeBitmap { + raw: raw.into(), + set: set, + } + } +} + +impl DnsPacketData for NsecTypeBitmap { + fn deserialize(data: &mut Cursor) -> ::errors::Result { + // remember raw encoding + let raw = { + let mut data: Cursor = data.clone(); + remaining_bytes(&mut data) + }; + let mut set = BTreeSet::new(); + let mut prev_window = None; + while data.has_remaining() { + let window_base = (data.get_u8() as u16) << 8; + ensure!(Some(window_base) > prev_window, "wrong nsec bitmap window order, {:?} <= {:?}", Some(window_base), prev_window); + prev_window = Some(window_base); + check_enough_data!(data, 1, "nsec bitmap window length"); + let window_len = data.get_u8() as u16; + ensure!(window_len <= 32, "nsec bitmap window too long"); + check_enough_data!(data, window_len as usize, "nsec bitmap window length"); + for i in 0..window_len { + let mut v = data.get_u8(); + for j in 0..7 { + if 0 != v & 0x80 { + set.insert(Type(window_base + i*8 + j)); + } + v <<= 1; + } + } + } + Ok(NsecTypeBitmap{ + raw: raw, + set: set, + }) + } +} + +impl DnsTextData for NsecTypeBitmap { + fn dns_parse(data: &mut &str) -> ::errors::Result { + let mut set = BTreeSet::new(); + skip_whitespace(data); + while !data.is_empty() { + let t = Type::dns_parse(data)?; + set.insert(t); + } + Ok(NsecTypeBitmap::from_set(set)) + } + + fn dns_format(&self, f: &mut DnsTextFormatter) -> fmt::Result { + for t in &self.set { + write!(f, "{}", t)?; + } + Ok(()) + } +} + +/// `base32hex` encoding without padding for text representation (RFC +/// 4648). whitespaces not allowed (not the last field). +/// +/// Between 1 and 255 bytes long. +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct NextHashedOwnerName(Bytes); + +impl DnsPacketData for NextHashedOwnerName { + fn deserialize(data: &mut Cursor) -> Result { + let text = short_blob(data)?; + ensure!(text.len() > 0, "NextHashedOwnerName must not be empty"); + Ok(NextHashedOwnerName(text)) + } +} + +impl DnsTextData for NextHashedOwnerName { + fn dns_parse(data: &mut &str) -> ::errors::Result { + let field = next_field(data)?; + let raw = BASE32HEX_NOPAD_ALLOW_WS.decode(field.as_bytes()) + .with_context(|e| e.context(format!("invalid base32hex (no padding): {:?}", field)))?; + ensure!(raw.len() > 0, "NextHashedOwnerName must not be empty"); + ensure!(raw.len() < 256, "NextHashedOwnerName field must be at most 255 bytes long"); + Ok(NextHashedOwnerName(raw.into())) + } + + fn dns_format(&self, f: &mut DnsTextFormatter) -> fmt::Result { + if self.0.is_empty() { return Err(fmt::Error); } + write!(f, "{}", BASE32HEX_NOPAD_ALLOW_WS.encode(&self.0)) + } +} diff --git a/lib/dnsbox-base/src/common_types/nxt.rs b/lib/dnsbox-base/src/common_types/nxt.rs new file mode 100644 index 0000000..b148dae --- /dev/null +++ b/lib/dnsbox-base/src/common_types/nxt.rs @@ -0,0 +1,94 @@ +use bytes::{Bytes, Buf}; +use common_types::Type; +use errors::*; +use ser::packet::{DnsPacketData, remaining_bytes}; +use ser::text::{DnsTextData, DnsTextFormatter, skip_whitespace}; +use std::collections::BTreeSet; +use std::fmt; +use std::io::Cursor; + +/// Type Bitmap for NXT. Last field in a record. +/// +/// RFC 2535 says for the bitmap: "If the zero bit of the type bit map +/// is a one, it indicates that a different format is being used which +/// will always be the case if a type number greater than 127 is +/// present." +/// +/// But this other format isn't specified anywhere... +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct NxtTypeBitmap { + raw: Bytes, + set: BTreeSet, +} + +impl NxtTypeBitmap { + pub fn from_set(set: BTreeSet) -> Result { + let mut raw = Vec::new(); + + for t in &set { + ensure!(t.0 > 0, "type 0 cannot be represented in NXT bitmap"); + ensure!(t.0 < 128, "type {} cannot be represented in NXT bitmap", t); + + let bit_ndx = t.0 as u8; + let byte_ndx = (bit_ndx / 8) as usize; + if byte_ndx >= raw.len() { + raw.resize(byte_ndx + 1, 0); + } + + let mask = 0x80 >> (bit_ndx % 8); + raw[byte_ndx] |= mask; + } + + Ok(NxtTypeBitmap { + raw: raw.into(), + set: set, + }) + } +} + +impl DnsPacketData for NxtTypeBitmap { + fn deserialize(data: &mut Cursor) -> Result { + // remember raw encoding + let raw = { + let mut data: Cursor = data.clone(); + remaining_bytes(&mut data) + }; + let mut set = BTreeSet::new(); + let mut current = 0; + while data.has_remaining() { + ensure!(current < 128, "NXT bitmap too long"); + let mut v = data.get_u8(); + for _ in 0..7 { + if 0 != v & 0x80 { + ensure!(0 != current, "Type 0 not allowed in NXT bitmap"); + set.insert(Type(current)); + } + v <<= 1; + current += 1; + } + } + Ok(NxtTypeBitmap{ + raw: raw, + set: set, + }) + } +} + +impl DnsTextData for NxtTypeBitmap { + fn dns_parse(data: &mut &str) -> Result { + let mut set = BTreeSet::new(); + skip_whitespace(data); + while !data.is_empty() { + let t = Type::dns_parse(data)?; + set.insert(t); + } + NxtTypeBitmap::from_set(set) + } + + fn dns_format(&self, f: &mut DnsTextFormatter) -> fmt::Result { + for t in &self.set { + write!(f, "{}", t)?; + } + Ok(()) + } +} diff --git a/lib/dnsbox-base/src/common_types/rr_type/mod.rs b/lib/dnsbox-base/src/common_types/rr_type/mod.rs index fbfd751..dfc9a60 100644 --- a/lib/dnsbox-base/src/common_types/rr_type/mod.rs +++ b/lib/dnsbox-base/src/common_types/rr_type/mod.rs @@ -1,8 +1,99 @@ +use bytes::Bytes; +use errors::*; +use ser::DnsPacketData; +use ser::text::{DnsTextData, DnsTextFormatter, next_field}; +use std::fmt; +use std::io::Cursor; +use records::registry::{name_to_type, type_name}; +use common_types::types; +use std::borrow::Cow; + #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] pub struct Type(pub u16); -impl ::ser::DnsPacketData for Type { - fn deserialize(data: &mut ::std::io::Cursor<::bytes::Bytes>) -> ::errors::Result { - Ok(Type(::ser::DnsPacketData::deserialize(data)?)) +impl Type { + pub fn name(self) -> Cow<'static, str> { + if let Some(name) = type_name(self) { + Cow::Borrowed(name) + } else { + Cow::Owned(format!("TYPE{}", self.0)) + } + } + + /// defined in RFC 1035 + pub fn well_known(self) -> bool { + // 0x0001 (A) ... 0x0010 (TXT) are defined in RFC 1035 + self.0 >= 0x0001 && self.0 <= 0x0010 + } + + /// require converting to canonical form for DNSSEC (i.e. names + /// must be converted to (ASCII) lower case, no compression) + /// + /// See https://tools.ietf.org/html/rfc4034#section-6.2 (updates RFC 3597). + pub fn canonical(self) -> bool { + match self { + types::NS => true, + types::MD => true, + types::MF => true, + types::CNAME => true, + types::SOA => true, + types::MB => true, + types::MG => true, + types::MR => true, + types::PTR => true, + // types::HINFO => true, // doesn't have a name in data + types::MINFO => true, + types::MX => true, + // types::HINFO => true, // see above, also duplicate in the RFCs + types::RP => true, + types::AFSDB => true, + types::RT => true, + types::NSAP_PTR => true, // not listed in the RFCs, but probably should be. + types::SIG => true, + types::PX => true, + types::NXT => true, + types::SRV => true, // moved up to match numeric order + types::NAPTR => true, + types::KX => true, + // types::SRV => true, // moved up to match numeric order + types::A6 => true, // moved up to match numeric order + types::DNAME => true, + // types::A6 => true, // moved up to match numeric order + types::RRSIG => true, + types::NSEC => true, + _ => false, + } + } +} + +impl fmt::Display for Type { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if let Some(name) = type_name(*self) { + write!(f, "{}", name) + } else { + write!(f, "TYPE{}", self.0) + } + } +} + +impl DnsPacketData for Type { + fn deserialize(data: &mut Cursor) -> Result { + Ok(Type(DnsPacketData::deserialize(data)?)) + } +} + +impl DnsTextData for Type { + fn dns_parse(data: &mut &str) -> Result { + let field = next_field(data)?; + if field.starts_with("TYPE") || field.starts_with("type") { + if let Ok(t) = field[4..].parse::() { + return Ok(Type(t)); + } + } + name_to_type(field).ok_or_else(|| format_err!("unknown type {:?}", field)) + } + + fn dns_format(&self, f: &mut DnsTextFormatter) -> fmt::Result { + write!(f, "{}", self) } } diff --git a/lib/dnsbox-base/src/common_types/text.rs b/lib/dnsbox-base/src/common_types/text.rs new file mode 100644 index 0000000..3d4f394 --- /dev/null +++ b/lib/dnsbox-base/src/common_types/text.rs @@ -0,0 +1,115 @@ +use bytes::{Bytes, Buf}; +use errors::*; +use ser::packet::{DnsPacketData, short_blob, remaining_bytes}; +use ser::text::*; +use std::fmt; +use std::io::Cursor; + +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct ShortText(Bytes); + +// RFC 1035 names this `` +impl DnsPacketData for ShortText { + fn deserialize(data: &mut Cursor) -> Result { + Ok(ShortText(short_blob(data)?)) + } +} + +impl DnsTextData for ShortText { + fn dns_parse(data: &mut &str) -> ::errors::Result { + let raw = next_quoted_field(data)?; + ensure!(raw.len() < 256, "short text must be at most 255 bytes long"); + Ok(ShortText(raw.into())) + } + + fn dns_format(&self, f: &mut DnsTextFormatter) -> fmt::Result { + write!(f, "{}", quote(&self.0)) + } +} + +/// RFC 1035 names this `One or more `. No following +/// field allowed. +/// +/// Used for TXT and SPF. +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct LongText(Vec); + +impl DnsPacketData for LongText { + fn deserialize(data: &mut Cursor) -> Result { + let mut texts = Vec::new(); + loop { + texts.push(short_blob(data)?); + if !data.has_remaining() { break; } + } + Ok(LongText(texts)) + } +} + +impl DnsTextData for LongText { + fn dns_parse(data: &mut &str) -> ::errors::Result { + let mut result = Vec::new(); + // `next_quoted_field` should skip trailing whitespace, we only + // need to skip the beginning whitespace for the first + // `is_empty` check. + skip_whitespace(data); + while !data.is_empty() { + let part = next_quoted_field(data)?; + ensure!(part.len() < 256, "long text component must be at most 255 bytes long"); + result.push(part.into()); + } + Ok(LongText(result)) + } + + fn dns_format(&self, f: &mut DnsTextFormatter) -> fmt::Result { + // actually emit as "multiple fields" (i.e. space separated) + for part in &self.0 { + write!(f, "{}", quote(part))?; + } + Ok(()) + } +} + +// similart to ShortText, but text output doesn't use quotes. +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct UnquotedShortText(Bytes); + +impl DnsPacketData for UnquotedShortText { + fn deserialize(data: &mut Cursor) -> Result { + Ok(UnquotedShortText(short_blob(data)?)) + } +} + +impl DnsTextData for UnquotedShortText { + fn dns_parse(data: &mut &str) -> ::errors::Result { + let raw = next_quoted_field(data)?; + ensure!(raw.len() < 256, "short text must be at most 255 bytes long"); + Ok(UnquotedShortText(raw.into())) + } + + fn dns_format(&self, f: &mut DnsTextFormatter) -> fmt::Result { + write!(f, "{}", escape(&self.0)) + } +} + +/// A single (possibly quoted) , but no length +/// restriction. +/// +/// Last field, but no whitespace allowed (unless quoted of course). +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct RemainingText(Bytes); + +impl DnsPacketData for RemainingText { + fn deserialize(data: &mut Cursor) -> Result { + Ok(RemainingText(remaining_bytes(data))) + } +} + +impl DnsTextData for RemainingText { + fn dns_parse(data: &mut &str) -> ::errors::Result { + Ok(RemainingText(next_quoted_field(data)?.into())) + } + + fn dns_format(&self, f: &mut DnsTextFormatter) -> fmt::Result { + write!(f, "{}", quote(&self.0)) + } +} diff --git a/lib/dnsbox-base/src/common_types/text/mod.rs b/lib/dnsbox-base/src/common_types/text/mod.rs deleted file mode 100644 index 3a3b744..0000000 --- a/lib/dnsbox-base/src/common_types/text/mod.rs +++ /dev/null @@ -1,21 +0,0 @@ -use bytes::{Bytes, Buf}; -use std::io::Cursor; - -use ser::DnsPacketData; -use errors::*; - -#[derive(Clone, Debug)] -pub struct ShortText(Bytes); - -// RFC 1035 names this `` -impl DnsPacketData for ShortText { - fn deserialize(data: &mut Cursor) -> Result { - check_enough_data!(data, 1, "ShortText length"); - let label_len = data.get_u8() as usize; - check_enough_data!(data, label_len, "ShortText content"); - let pos = data.position() as usize; - let text = data.get_ref().slice(pos, pos + label_len); - data.advance(label_len); - Ok(ShortText(text)) - } -} diff --git a/lib/dnsbox-base/src/common_types/time.rs b/lib/dnsbox-base/src/common_types/time.rs new file mode 100644 index 0000000..99e065f --- /dev/null +++ b/lib/dnsbox-base/src/common_types/time.rs @@ -0,0 +1,38 @@ +use bytes::Bytes; +use errors::*; +use ser::packet::DnsPacketData; +use ser::text::{DnsTextData, DnsTextFormatter, next_field}; +use std::fmt; +use std::io::Cursor; + +/// A single quoted non-empty URL. +/// +/// Actually shouldn't allow escapes (i.e. no backslash in the content); +/// but to make sure we can export and import any data we allow standard +/// escape mechanisms and even accept unquoted input. +/// +/// No whitespace allowed, last field. +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct Time(u32); + +impl DnsPacketData for Time { + fn deserialize(data: &mut Cursor) -> Result { + Ok(Time(DnsPacketData::deserialize(data)?)) + } +} + +impl DnsTextData for Time { + fn dns_parse(data: &mut &str) -> ::errors::Result { + let field = next_field(data)?; + let epoch = field.parse::(); + if field.len() == 14 && epoch.is_err() { + unimplemented!() + } else { + Ok(Time(epoch?)) + } + } + + fn dns_format(&self, f: &mut DnsTextFormatter) -> fmt::Result { + write!(f, "{}", self.0) + } +} diff --git a/lib/dnsbox-base/src/records/types.rs b/lib/dnsbox-base/src/common_types/types.rs similarity index 100% rename from lib/dnsbox-base/src/records/types.rs rename to lib/dnsbox-base/src/common_types/types.rs diff --git a/lib/dnsbox-base/src/common_types/uri.rs b/lib/dnsbox-base/src/common_types/uri.rs new file mode 100644 index 0000000..1ab94f8 --- /dev/null +++ b/lib/dnsbox-base/src/common_types/uri.rs @@ -0,0 +1,36 @@ +use bytes::Bytes; +use errors::*; +use ser::packet::{DnsPacketData, remaining_bytes}; +use ser::text::*; +use std::fmt; +use std::io::Cursor; + +/// A single quoted non-empty URL. +/// +/// Actually shouldn't allow escapes (i.e. no backslash in the content); +/// but to make sure we can export and import any data we allow standard +/// escape mechanisms and even accept unquoted input. +/// +/// No whitespace allowed, last field. +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct UriText(Bytes); + +impl DnsPacketData for UriText { + fn deserialize(data: &mut Cursor) -> Result { + let raw = remaining_bytes(data); + ensure!(!raw.is_empty(), "URI must not be empty"); + Ok(UriText(raw)) + } +} + +impl DnsTextData for UriText { + fn dns_parse(data: &mut &str) -> ::errors::Result { + let raw = next_quoted_field(data)?; + ensure!(!raw.is_empty(), "URI must not be empty"); + Ok(UriText(raw.into())) + } + + fn dns_format(&self, f: &mut DnsTextFormatter) -> fmt::Result { + write!(f, "{}", quote(&self.0)) + } +} diff --git a/lib/dnsbox-base/src/lib.rs b/lib/dnsbox-base/src/lib.rs index 5efc869..f91621a 100644 --- a/lib/dnsbox-base/src/lib.rs +++ b/lib/dnsbox-base/src/lib.rs @@ -1,5 +1,6 @@ pub extern crate byteorder; pub extern crate bytes; +pub extern crate data_encoding; #[macro_use] pub extern crate failure; @@ -9,6 +10,8 @@ extern crate smallvec; #[macro_use] extern crate lazy_static; +mod unsafe_ops; + #[macro_use] pub mod errors; pub mod common_types; diff --git a/lib/dnsbox-base/src/records/mod.rs b/lib/dnsbox-base/src/records/mod.rs index 8cc14b4..23ed95a 100644 --- a/lib/dnsbox-base/src/records/mod.rs +++ b/lib/dnsbox-base/src/records/mod.rs @@ -1,8 +1,10 @@ +mod weird_structs; mod structs; +mod unknown; pub mod registry; -pub mod types; pub use self::structs::*; +pub use self::unknown::*; #[cfg(test)] mod tests; \ No newline at end of file diff --git a/lib/dnsbox-base/src/records/registry.rs b/lib/dnsbox-base/src/records/registry.rs index 04c6494..b48198d 100644 --- a/lib/dnsbox-base/src/records/registry.rs +++ b/lib/dnsbox-base/src/records/registry.rs @@ -1,9 +1,11 @@ use std::collections::HashMap; +use std::ascii::AsciiExt; +use std::marker::PhantomData; use records::structs; -use records::types; +use common_types::types; use common_types::Type; -use ser::RRData; +use ser::{RRData, StaticRRData}; lazy_static!{ static ref REGISTRY: Registry = Registry::init(); @@ -15,7 +17,7 @@ fn registry() -> &'static Registry { pub fn known_name_to_type(name: &str) -> Option { let registry = registry(); - let &t = registry.names_to_type.get(name)?; + let t = name_to_type(name)?; registry.type_parser.get(&t)?; Some(t) @@ -27,8 +29,16 @@ pub fn name_to_type(name: &str) -> Option { Some(t) } +pub(crate) fn type_name(rrtype: Type) -> Option<&'static str> { + let registry = registry(); + registry.type_names.get(&rrtype).map(|s| s as _) +} + +struct TagRRDataType(PhantomData); + struct Registry { names_to_type: HashMap, + type_names: HashMap, type_parser: HashMap, // make sure registrations are in order prev_type: Option, @@ -38,6 +48,7 @@ impl Registry { fn init() -> Self { let mut r = Registry { names_to_type: HashMap::new(), + type_names: HashMap::new(), type_parser: HashMap::new(), prev_type: None, }; @@ -51,8 +62,8 @@ impl Registry { r.register_known::(); r.register_known::(); r.register_known::(); - r.register_known::(); - r.register_known::(); + r.register_unknown("NULL" , types::NULL); + r.register_unknown("WKS" , types::WKS); r.register_known::(); r.register_known::(); r.register_known::(); @@ -62,16 +73,16 @@ impl Registry { r.register_known::(); r.register_unknown("X25" , types::X25); r.register_unknown("ISDN" , types::ISDN); - r.register_unknown("RT" , types::RT); + r.register_known::(); r.register_unknown("NSAP" , types::NSAP); - r.register_unknown("NSAP-PTR" , types::NSAP_PTR); - r.register_unknown("SIG" , types::SIG); + r.register_known::(); + r.register_known::(); r.register_known::(); - r.register_unknown("PX" , types::PX); + r.register_known::(); r.register_unknown("GPOS" , types::GPOS); r.register_known::(); r.register_known::(); - r.register_unknown("NXT" , types::NXT); + r.register_known::(); r.register_unknown("EID" , types::EID); r.register_unknown("NIMLOC" , types::NIMLOC); r.register_known::(); @@ -79,7 +90,7 @@ impl Registry { r.register_known::(); r.register_known::(); r.register_known::(); - r.register_unknown("A6" , types::A6); + r.register_known::(); r.register_known::(); r.register_unknown("SINK" , types::SINK); r.register_unknown("OPT" , types::OPT); @@ -121,8 +132,8 @@ impl Registry { r.register_unknown("MAILB" , types::MAILB); r.register_unknown("MAILA" , types::MAILA); r.register_unknown("ANY" , types::ANY); - r.register_unknown("URI" , types::URI); - r.register_unknown("CAA" , types::CAA); + r.register_known::(); + r.register_known::(); r.register_unknown("AVC" , types::AVC); r.register_unknown("DOA" , types::DOA); r.register_unknown("DLV" , types::DLV); @@ -132,18 +143,23 @@ impl Registry { r } - fn register_unknown(&mut self, name: &'static str, rrtype: Type) { + fn register_name(&mut self, name: &str, rrtype: Type) { assert!(self.prev_type < Some(rrtype), "registration not in order"); self.prev_type = Some(rrtype); - assert!(self.names_to_type.insert(name.into(), rrtype).is_none()); + let mut name: String = name.into(); + name.make_ascii_uppercase(); + assert!(self.names_to_type.insert(name.clone(), rrtype).is_none()); + self.type_names.insert(rrtype, name); } - fn register_known(&mut self) { - let n = T::rr_type_name(); - let t = T::rr_type(); - assert!(self.prev_type < Some(t), "registration not in order"); - self.prev_type = Some(t); - assert!(self.names_to_type.insert(n.into_owned(), t).is_none()); - self.type_parser.insert(t, ()); + fn register_unknown(&mut self, name: &'static str, rrtype: Type) { + self.register_name(name, rrtype); + } + + fn register_known(&mut self) { + let rrtype = T::TYPE; + let name = T::NAME; + self.register_name(name, rrtype); + self.type_parser.insert(rrtype, ()); } } diff --git a/lib/dnsbox-base/src/records/structs.rs b/lib/dnsbox-base/src/records/structs.rs index c3b0c3b..9d6a196 100644 --- a/lib/dnsbox-base/src/records/structs.rs +++ b/lib/dnsbox-base/src/records/structs.rs @@ -1,42 +1,44 @@ -use bytes::Bytes; -use ser::DnsPacketData; use common_types::*; use std::net::{Ipv4Addr, Ipv6Addr}; // unless otherwise documented, class should probably be IN (0x0001) +// deriving RRData will add a unit test to make sure the type is +// registered; there must be a records::types::$name `Type` constant +// with the same name as the struct. + // class IN -#[derive(Clone, Debug, DnsPacketData, RRData)] +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, DnsPacketData, DnsTextData, RRData)] pub struct A { addr: Ipv4Addr, } // class independent -#[derive(Clone, Debug, DnsPacketData, RRData)] +#[derive(Clone, PartialEq, Eq, Debug, DnsPacketData, DnsTextData, RRData)] pub struct NS { nsdname: DnsCompressedName, } // class independent -#[derive(Clone, Debug, DnsPacketData, RRData)] +#[derive(Clone, PartialEq, Eq, Debug, DnsPacketData, DnsTextData, RRData)] pub struct MD { madname: DnsCompressedName, } // class independent -#[derive(Clone, Debug, DnsPacketData, RRData)] +#[derive(Clone, PartialEq, Eq, Debug, DnsPacketData, DnsTextData, RRData)] pub struct MF { madname: DnsCompressedName, } // class independent -#[derive(Clone, Debug, DnsPacketData, RRData)] +#[derive(Clone, PartialEq, Eq, Debug, DnsPacketData, DnsTextData, RRData)] pub struct CNAME { cname: DnsCompressedName, } // class independent -#[derive(Clone, Debug, DnsPacketData, RRData)] +#[derive(Clone, PartialEq, Eq, Debug, DnsPacketData, DnsTextData, RRData)] pub struct SOA { mname: DnsCompressedName, rname: DnsCompressedName, @@ -48,66 +50,68 @@ pub struct SOA { } // class independent -#[derive(Clone, Debug, DnsPacketData, RRData)] +#[derive(Clone, PartialEq, Eq, Debug, DnsPacketData, DnsTextData, RRData)] pub struct MB { madname: DnsCompressedName, } // class independent -#[derive(Clone, Debug, DnsPacketData, RRData)] +#[derive(Clone, PartialEq, Eq, Debug, DnsPacketData, DnsTextData, RRData)] pub struct MG { mgmname: DnsCompressedName, } // class independent -#[derive(Clone, Debug, DnsPacketData, RRData)] +#[derive(Clone, PartialEq, Eq, Debug, DnsPacketData, DnsTextData, RRData)] pub struct MR { newname: DnsCompressedName, } -// class independent -#[derive(Clone, Debug, DnsPacketData, RRData)] -pub struct NULL { - anything: RemainingText, -} +// not allowed in zone files anyway, i.e. no text representation. +// content not restricted either, just some bytes. no need to parse it. +// +// class independent pub struct NULL; +// text representation like: `WKS 127.0.0.1 TCP smtp http 110`. would +// have to parse protocol and service names. +// // class IN -#[derive(Clone, Debug, DnsPacketData, RRData)] -pub struct WKS { - address: Ipv4Addr, - protocol: u8, - bitmap: RemainingText, -} +// #[derive(Clone, PartialEq, Eq, Debug, DnsPacketData, DnsTextData, RRData)] +// pub struct WKS { +// address: Ipv4Addr, +// protocol: u8, +// bitmap: ..., // remaining bytes +// } // class independent -#[derive(Clone, Debug, DnsPacketData, RRData)] +#[derive(Clone, PartialEq, Eq, Debug, DnsPacketData, DnsTextData, RRData)] pub struct PTR { ptrdname: DnsCompressedName, } // class independent -#[derive(Clone, Debug, DnsPacketData, RRData)] +#[derive(Clone, PartialEq, Eq, Debug, DnsPacketData, DnsTextData, RRData)] pub struct HINFO { cpu: ShortText, os: ShortText, } // class independent -#[derive(Clone, Debug, DnsPacketData, RRData)] +#[derive(Clone, PartialEq, Eq, Debug, DnsPacketData, DnsTextData, RRData)] pub struct MINFO { rmailbx: DnsCompressedName, emailbx: DnsCompressedName, } // class independent -#[derive(Clone, Debug, DnsPacketData, RRData)] +#[derive(Clone, PartialEq, Eq, Debug, DnsPacketData, DnsTextData, RRData)] pub struct MX { preference: u16, mxname: DnsCompressedName, } // class independent -#[derive(Clone, Debug, DnsPacketData, RRData)] +#[derive(Clone, PartialEq, Eq, Debug, DnsPacketData, DnsTextData, RRData)] pub struct TXT { text: LongText, } @@ -115,132 +119,153 @@ pub struct TXT { // end of RFC 1035: no DnsCompressedName below! // class independent -#[derive(Clone, Debug, DnsPacketData, RRData)] +#[derive(Clone, PartialEq, Eq, Debug, DnsPacketData, DnsTextData, RRData)] pub struct RP { - mbox: DnsName, - txt: DnsName, + mbox: DnsCanonicalName, + txt: DnsCanonicalName, } // class independent -#[derive(Clone, Debug, DnsPacketData, RRData)] +#[derive(Clone, PartialEq, Eq, Debug, DnsPacketData, DnsTextData, RRData)] pub struct AFSDB { subtype: u16, - hostname: DnsName, + hostname: DnsCanonicalName, +} + +// https://tools.ietf.org/html/rfc1183#section-3.1 says "its format in +// master files is a " which say nothing about the +// binary encoding; later it says " is a string of decimal +// digits", so it would seem that there is no length encoding or +// restriction. +// +// wireshark and bind use though (bind also wants at +// least 4 bytes in the field: probably due to "beginning with the 4 +// digit DNIC"). +// +// class independent +// pub struct X25 { +// psdn_address: ShortText, +// } + +// class independent +// pub struct ISDN { +// isdn_address: ShortText, +// subaddress: Option, +// } + +// class independent +#[derive(Clone, PartialEq, Eq, Debug, DnsPacketData, DnsTextData, RRData)] +pub struct RT { + preference: u16, + intermediate: DnsCanonicalName, +} + +// pub struct NSAP; + +#[allow(non_camel_case_types)] +#[derive(Clone, PartialEq, Eq, Debug, DnsPacketData, DnsTextData, RRData)] +#[RRTypeName = "NSAP-PTR"] +pub struct NSAP_PTR { + owner: DnsCanonicalName, } // class independent -// pub struct X25; +#[derive(Clone, PartialEq, Eq, Debug, DnsPacketData, DnsTextData, RRData)] +pub struct SIG { + rr_type: Type, + algorithm: u8, + labels: u8, + // RFC says this can be omitted in text form if it is the same as + // the TTL on the SIG record. not supported to be omitted here + // (TODO?). + original_ttl: u32, + signature_expiration: Time, + signature_inception: Time, + key_tag: u16, + signers_name: DnsCanonicalName, + signature: Base64RemainingBlob, +} // class independent -// pub struct ISDN; - -// class independent -// pub struct RT; - -// pub struct NSAP; -// pub struct NSAP_PTR; -// pub struct SIG; - -// class independent -#[derive(Clone, Debug, DnsPacketData, RRData)] +#[derive(Clone, PartialEq, Eq, Debug, DnsPacketData, DnsTextData, RRData)] pub struct KEY { flags: u16, protocol: u8, algorithm: u8, - certificate: RemainingText, + public_key: Base64RemainingBlob, +} + +// class IN +#[derive(Clone, PartialEq, Eq, Debug, DnsPacketData, DnsTextData, RRData)] +pub struct PX { + preference: u16, + map822: DnsCanonicalName, + mapx400: DnsCanonicalName, } -// pub struct PX; // pub struct GPOS; // class IN -#[derive(Clone, Debug, DnsPacketData, RRData)] +#[derive(Clone, PartialEq, Eq, Debug, DnsPacketData, DnsTextData, RRData)] pub struct AAAA { addr: Ipv6Addr, } -// class independent -#[derive(Clone, Debug, RRData)] -pub enum LOC { - Version0(LOC0), - UnknownVersion{ - version: u8, - data: RemainingText - }, +pub use super::weird_structs::LOC; + +#[derive(Clone, PartialEq, Eq, Debug, DnsPacketData, DnsTextData, RRData)] +pub struct NXT { + next: DnsCanonicalName, + types: NxtTypeBitmap, } -impl DnsPacketData for LOC { - fn deserialize(data: &mut ::std::io::Cursor) -> ::errors::Result { - let version: u8 = DnsPacketData::deserialize(data)?; - if 0 == version { - Ok(LOC::Version0(DnsPacketData::deserialize(data)?)) - } else { - Ok(LOC::UnknownVersion{ - version: version, - data: DnsPacketData::deserialize(data)?, - }) - } - } -} - -#[derive(Clone, Debug, DnsPacketData)] -pub struct LOC0 { - size: u8, - horizontal_precision: u8, - vertical_precision: u8, - latitude: u32, - longitude: u32, - altitude: u32, -} - -// pub struct NXT; // pub struct EID; // pub struct NIMLOC; // class IN -#[derive(Clone, Debug, DnsPacketData, RRData)] +#[derive(Clone, PartialEq, Eq, Debug, DnsPacketData, DnsTextData, RRData)] pub struct SRV { preference: u16, weight: u16, port: u16, - target: DnsName, + target: DnsCanonicalName, } // pub struct ATMA; // class independent -#[derive(Clone, Debug, DnsPacketData, RRData)] +#[derive(Clone, PartialEq, Eq, Debug, DnsPacketData, DnsTextData, RRData)] pub struct NAPTR { order: u16, preference: u16, flags: ShortText, service: ShortText, regexp: ShortText, - replacement: DnsName, + replacement: DnsCanonicalName, } // class IN -#[derive(Clone, Debug, DnsPacketData, RRData)] +#[derive(Clone, PartialEq, Eq, Debug, DnsPacketData, DnsTextData, RRData)] pub struct KX { preference: u16, - exchanger: DnsName, + exchanger: DnsCanonicalName, } // class ?? -#[derive(Clone, Debug, DnsPacketData, RRData)] +#[derive(Clone, PartialEq, Eq, Debug, DnsPacketData, DnsTextData, RRData)] pub struct CERT { cert_type: u16, key_tag: u16, algorithm: u8, - certificate: RemainingText, + certificate: Base64RemainingBlob, } -// pub struct A6; +pub use super::weird_structs::A6; // class independent -#[derive(Clone, Debug, DnsPacketData, RRData)] +#[derive(Clone, PartialEq, Eq, Debug, DnsPacketData, DnsTextData, RRData)] pub struct DNAME { - target: DnsName, + target: DnsCanonicalName, } // pub struct SINK; @@ -251,7 +276,7 @@ pub struct DNAME { // pub struct APL; -#[derive(Clone, Debug, DnsPacketData, RRData)] +#[derive(Clone, PartialEq, Eq, Debug, DnsPacketData, DnsTextData, RRData)] pub struct DS { key_tag: u16, algorithm: u8, @@ -259,67 +284,64 @@ pub struct DS { digest: HexRemainingBlob, } -#[derive(Clone, Debug, DnsPacketData, RRData)] +#[derive(Clone, PartialEq, Eq, Debug, DnsPacketData, DnsTextData, RRData)] pub struct SSHFP { algorithm: u8, fingerprint_type: u8, + // RFC 4255 doesn't specify whether whitespace is allowed. + // `HexRemainingBlob` allows whitespace. fingerprint: HexRemainingBlob, } -// #[derive(Clone, Debug, DnsPacketData, RRData)] +// #[derive(Clone, PartialEq, Eq, Debug, DnsPacketData, DnsTextData, RRData)] // pub struct IPSECKEY; // class independent -#[derive(Clone, Debug, DnsPacketData, RRData)] +#[derive(Clone, PartialEq, Eq, Debug, DnsPacketData, DnsTextData, RRData)] pub struct RRSIG { rr_type: Type, algorithm: u8, labels: u8, original_ttl: u32, - signature_expiration: u32, - signature_inception: u32, + signature_expiration: Time, + signature_inception: Time, key_tag: u16, - signers_name: DnsName, - signature: RemainingText, -} - -#[derive(Clone, Debug, DnsPacketData)] -pub struct NsecTypeBitmap { - types: RemainingText, // TODO: actually parse it + signers_name: DnsCanonicalName, + signature: Base64RemainingBlob, } // class independent -#[derive(Clone, Debug, DnsPacketData, RRData)] +#[derive(Clone, PartialEq, Eq, Debug, DnsPacketData, DnsTextData, RRData)] pub struct NSEC { - next: DnsName, - bitmap: NsecTypeBitmap, + next: DnsCanonicalName, + types: NsecTypeBitmap, } // class independent -#[derive(Clone, Debug, DnsPacketData, RRData)] +#[derive(Clone, PartialEq, Eq, Debug, DnsPacketData, DnsTextData, RRData)] pub struct DNSKEY { flags: u16, protocol: u8, algorithm: u8, - public_key: RemainingText, + public_key: Base64RemainingBlob, } -// #[derive(Clone, Debug, DnsPacketData, RRData)] +// #[derive(Clone, PartialEq, Eq, Debug, DnsPacketData, DnsTextData, RRData)] // pub struct DHCID; // class independent -#[derive(Clone, Debug, DnsPacketData, RRData)] +#[derive(Clone, PartialEq, Eq, Debug, DnsPacketData, DnsTextData, RRData)] pub struct NSEC3 { hash_algorithm: u8, flags: u8, iterations: u16, salt: HexShortBlob, - next_hashed: Base32ShortBlob, - bitmap: NsecTypeBitmap, + next_hashed: NextHashedOwnerName, + types: NsecTypeBitmap, } // class independent -#[derive(Clone, Debug, DnsPacketData, RRData)] +#[derive(Clone, PartialEq, Eq, Debug, DnsPacketData, DnsTextData, RRData)] pub struct NSEC3PARAM { hash_algorithm: u8, flags: u8, @@ -327,32 +349,32 @@ pub struct NSEC3PARAM { salt: HexShortBlob, } -// #[derive(Clone, Debug, DnsPacketData, RRData)] +// #[derive(Clone, PartialEq, Eq, Debug, DnsPacketData, DnsTextData, RRData)] // pub struct TLSA; -// #[derive(Clone, Debug, DnsPacketData, RRData)] +// #[derive(Clone, PartialEq, Eq, Debug, DnsPacketData, DnsTextData, RRData)] // pub struct SMIMEA; // pub struct HIP; // pub struct NINFO; -// #[derive(Clone, Debug, DnsPacketData, RRData)] +// #[derive(Clone, PartialEq, Eq, Debug, DnsPacketData, DnsTextData, RRData)] // pub struct RKEY; // pub struct TALINK; -// #[derive(Clone, Debug, DnsPacketData, RRData)] +// #[derive(Clone, PartialEq, Eq, Debug, DnsPacketData, DnsTextData, RRData)] // pub struct CDS; -// #[derive(Clone, Debug, DnsPacketData, RRData)] +// #[derive(Clone, PartialEq, Eq, Debug, DnsPacketData, DnsTextData, RRData)] // pub struct CDNSKEY; -// #[derive(Clone, Debug, DnsPacketData, RRData)] +// #[derive(Clone, PartialEq, Eq, Debug, DnsPacketData, DnsTextData, RRData)] // pub struct OPENPGPKEY; // pub struct CSYNC; -#[derive(Clone, Debug, DnsPacketData, RRData)] +#[derive(Clone, PartialEq, Eq, Debug, DnsPacketData, DnsTextData, RRData)] pub struct SPF { text: LongText, } @@ -366,16 +388,16 @@ pub struct SPF { // pub struct L64; // pub struct LP; -// #[derive(Clone, Debug, DnsPacketData, RRData)] +// #[derive(Clone, PartialEq, Eq, Debug, DnsPacketData, DnsTextData, RRData)] // pub struct EUI48; -// #[derive(Clone, Debug, DnsPacketData, RRData)] +// #[derive(Clone, PartialEq, Eq, Debug, DnsPacketData, DnsTextData, RRData)] // pub struct EUI64; -// #[derive(Clone, Debug, DnsPacketData, RRData)] +// #[derive(Clone, PartialEq, Eq, Debug, DnsPacketData, DnsTextData, RRData)] // pub struct TKEY; -// #[derive(Clone, Debug, DnsPacketData, RRData)] +// #[derive(Clone, PartialEq, Eq, Debug, DnsPacketData, DnsTextData, RRData)] // pub struct TSIG; // pub struct IXFR; // qtype only? @@ -384,19 +406,27 @@ pub struct SPF { // pub struct MAILA; // qtype only? // pub struct ANY; // qtype only? -// #[derive(Clone, Debug, DnsPacketData, RRData)] -// pub struct URI; +#[derive(Clone, PartialEq, Eq, Debug, DnsPacketData, DnsTextData, RRData)] +pub struct URI { + priority: u16, + weight: u16, + target: UriText, +} -// #[derive(Clone, Debug, DnsPacketData, RRData)] -// pub struct CAA; +#[derive(Clone, PartialEq, Eq, Debug, DnsPacketData, DnsTextData, RRData)] +pub struct CAA { + flags: u8, + tag: UnquotedShortText, + value: RemainingText, +} // pub struct AVC; // pub struct DOA; -// #[derive(Clone, Debug, DnsPacketData, RRData)] +// #[derive(Clone, PartialEq, Eq, Debug, DnsPacketData, DnsTextData, RRData)] // pub struct DLV; // pub struct ADDR; -// #[derive(Clone, Debug, DnsPacketData, RRData)] +// #[derive(Clone, PartialEq, Eq, Debug, DnsPacketData, DnsTextData, RRData)] // pub struct ALIAS; diff --git a/lib/dnsbox-base/src/records/tests.rs b/lib/dnsbox-base/src/records/tests.rs index 49d3564..d303cc0 100644 --- a/lib/dnsbox-base/src/records/tests.rs +++ b/lib/dnsbox-base/src/records/tests.rs @@ -1,16 +1,118 @@ -use bytes::Bytes; +use bytes::{Bytes, Buf}; +use failure::ResultExt; use records::structs; -use ser::packet::deserialize; -use ser::RRData; +use ser::{packet, text, StaticRRData}; +use std::fmt; +use std::io::Cursor; -fn check(txt: &str, data: &'static [u8]) +fn rrdata_de(data: &'static [u8]) -> ::errors::Result where - T: RRData + T: StaticRRData { - let d: T = deserialize(Bytes::from_static(data)).expect("couldn't parse record"); + let mut data = Cursor::new(Bytes::from_static(data)); + let result = T::deserialize_rr_data(3600, 0x0001, T::TYPE, &mut data)?; + ensure!(!data.has_remaining(), "rrdata not read completely"); + Ok(result) +} + +fn check(txt: &str, data: &'static [u8]) -> ::errors::Result<()> +where + T: StaticRRData + fmt::Debug + PartialEq +{ + let d1: T = rrdata_de(data).context("couldn't parse binary record")?; + let d2: T = text::parse(txt).context("couldn't parse text record")?; + ensure!(d1 == d2, "decoded data not equal: {:?} != {:?}", d1, d2); + Ok(()) +} + +fn check2(txt: &str, data: &'static [u8], canon: &str) -> ::errors::Result<()> +where + T: StaticRRData + fmt::Debug + PartialEq +{ + let d1: T = rrdata_de(data).context("couldn't parse binary record")?; + let d2: T = text::parse(txt).context("couldn't parse text record")?; + ensure!(d1 == d2, "decoded data not equal: {:?} != {:?}", d1, d2); + + Ok(()) } #[test] fn test_a() { - check::("127.0.0.1", b"\x7f\x00\x00\x01"); + check::("127.0.0.1", b"\x7f\x00\x00\x01").unwrap(); +} + +fn test_txt_for() +where + T: StaticRRData + fmt::Debug + PartialEq +{ + // at least one "segment" (which could be empty) + check2::(r#" "" "#, b"", r#" "" "#).unwrap_err(); + check2::(r#""#, b"\x00", r#" "" "#).unwrap_err(); + // one empty segment + check2::(r#" "" "#, b"\x00", r#" "" "#).unwrap(); + // one segment + check::(r#" "foo" "#, b"\x03foo").unwrap(); + // two segments + check::(r#" "foo" "bar!" "#, b"\x03foo\x04bar!").unwrap(); + // segment with too many bytes in text form + { + let mut s = String::new(); + s.push('"'); + for _ in 0..256 { s.push('a'); } + s.push('"'); + text::parse::(&s).unwrap_err(); + } +} + +#[test] +fn test_txt() { + test_txt_for::(); +} + +#[test] +fn test_ds() { + check::(" 1 2 3 ", b"\x00\x01\x02\x03").unwrap(); + check::(" 1 2 3 abcd", b"\x00\x01\x02\x03\xab\xcd").unwrap(); + check::(" 1 2 3 a b c d", b"\x00\x01\x02\x03\xab\xcd").unwrap(); +} + +#[test] +fn test_nsec() { + check::("foo.bar. ", b"\x03foo\x03bar\x00").unwrap(); + check::("foo.bar. A NS ", b"\x03foo\x03bar\x00\x00\x01\x60").unwrap(); + check::("foo.bar. A NS TYPE256 TYPE65280 ", b"\x03foo\x03bar\x00\x00\x01\x60\x01\x01\x80\xff\x01\x80").unwrap(); +} + +#[test] +fn test_dnskey() { + check::("256 2 3", b"\x01\x00\x02\x03").unwrap(); + check::("256 2 3 /w==", b"\x01\x00\x02\x03\xff").unwrap(); + check::("256 2 3 /w ==", b"\x01\x00\x02\x03\xff").unwrap(); +} + +#[test] +fn test_nsec3() { + check::("1 2 300 - vs", b"\x01\x02\x01\x2c\x00\x01\xff").unwrap(); + check::("1 2 300 - vs A NS", b"\x01\x02\x01\x2c\x00\x01\xff\x00\x01\x60").unwrap(); + check::("1 2 300 ab vs A NS", b"\x01\x02\x01\x2c\x01\xab\x01\xff\x00\x01\x60").unwrap(); + + // invalid base32 texts + text::parse::("1 2 300 - v").unwrap_err(); + text::parse::("1 2 300 - vv").unwrap_err(); + + // invalid (empty) next-hashed values + packet::deserialize::(Bytes::from_static(b"\x01\x02\x01\x2c\x00\x00")).unwrap_err(); +} + +#[test] +fn test_nsec3param() { + check::("1 2 300 -", b"\x01\x02\x01\x2c\x00").unwrap(); + check::("1 2 300 ab", b"\x01\x02\x01\x2c\x01\xab").unwrap(); + // `salt` hex string must not contain spaces + text::parse::("1 2 300 a b").unwrap_err(); +} + +#[test] +fn test_spf() { + test_txt_for::(); } diff --git a/lib/dnsbox-base/src/records/unknown.rs b/lib/dnsbox-base/src/records/unknown.rs new file mode 100644 index 0000000..4b32011 --- /dev/null +++ b/lib/dnsbox-base/src/records/unknown.rs @@ -0,0 +1,47 @@ +use bytes::Bytes; +use common_types::*; +use failure::{ResultExt, Fail}; +use ser::text::{DnsTextFormatter, next_field}; +use ser::packet::remaining_bytes; +use common_types::binary::HEXLOWER_PERMISSIVE_ALLOW_WS; +use std::fmt; +use errors::*; + +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct UnknownRecord { + rr_type: Type, + raw: Bytes, +} + +impl UnknownRecord { + pub fn new(rr_type: Type, raw: Bytes) -> Self { + UnknownRecord { + rr_type: rr_type, + raw: raw, + } + } + + pub fn deserialize(rr_type: Type, data: &mut ::std::io::Cursor) -> Result { + Ok(UnknownRecord::new(rr_type, remaining_bytes(data))) + } + + pub fn dns_parse(rr_type: Type, data: &mut &str) -> Result { + let field = next_field(data)?; + ensure!(field == r"\#", "expect \\# token to mark generic encoding"); + let field = next_field(data).context("generic record data length")?; + let len: usize = field.parse()?; + + let result = HEXLOWER_PERMISSIVE_ALLOW_WS.decode(data.as_bytes()) + .with_context(|e| e.context(format!("invalid hex: {:?}", data)))?; + ensure!(len == result.len(), "length {} doesn't match length of encoded data {}", len, result.len()); + + Ok(UnknownRecord { + rr_type, + raw: result.into(), + }) + } + + pub fn dns_format(&self, f: &mut DnsTextFormatter) -> fmt::Result { + write!(f, "TYPE{} \\# {} {}", self.rr_type.0, self.raw.len(), HEXLOWER_PERMISSIVE_ALLOW_WS.encode(&self.raw)) + } +} diff --git a/lib/dnsbox-base/src/records/weird_structs.rs b/lib/dnsbox-base/src/records/weird_structs.rs new file mode 100644 index 0000000..7bac790 --- /dev/null +++ b/lib/dnsbox-base/src/records/weird_structs.rs @@ -0,0 +1,153 @@ +use bytes::{Bytes, Buf}; +use common_types::*; +use failure::ResultExt; +use ser::DnsPacketData; +use ser::text::{DnsTextData, DnsTextFormatter}; +use std::fmt; +use std::io::Read; +use std::net::Ipv6Addr; + +// deriving RRData will add a unit test to make sure the type is +// registered; there must be a records::types::$name `Type` constant +// with the same name as the struct. + +// class independent +#[derive(Clone, PartialEq, Eq, Debug, RRData)] +pub enum LOC { + Version0(LOC0), + UnknownVersion{ + version: u8, + data: Bytes, + }, +} + +impl DnsPacketData for LOC { + fn deserialize(data: &mut ::std::io::Cursor) -> ::errors::Result { + let version: u8 = DnsPacketData::deserialize(data)?; + if 0 == version { + Ok(LOC::Version0(DnsPacketData::deserialize(data)?)) + } else { + Ok(LOC::UnknownVersion{ + version: version, + data: ::ser::packet::remaining_bytes(data), + }) + } + } +} + +impl DnsTextData for LOC { + fn dns_parse(_data: &mut &str) -> ::errors::Result { + unimplemented!() + } + + fn dns_format(&self, _f: &mut DnsTextFormatter) -> fmt::Result { + // always prefer binary representation + Err(fmt::Error) + } +} + +#[derive(Clone, PartialEq, Eq, Debug, DnsPacketData)] +pub struct LOC0 { + size: u8, + horizontal_precision: u8, + vertical_precision: u8, + latitude: u32, + longitude: u32, + altitude: u32, +} + +// class IN +#[derive(Clone, PartialEq, Eq, Debug, RRData)] +pub struct A6 { + prefix: u8, // [0...128] + // might include non-zero padding + dirty_suffix: Ipv6Addr, + suffix: Ipv6Addr, + prefix_name: Option, +} + +impl DnsPacketData for A6 { + fn deserialize(data: &mut ::std::io::Cursor) -> ::errors::Result { + let prefix: u8 = DnsPacketData::deserialize(data) + .context("failed parsing field A6::prefix")?; + ensure!(prefix <= 128, "invalid A6::prefix {}", prefix); + let suffix_offset = (prefix / 8) as usize; + debug_assert!(suffix_offset <= 16); + let suffix_len = 16 - suffix_offset; + check_enough_data!(data, suffix_len, "A6::suffix"); + let mut addr = [0u8; 16]; + data.read_exact(&mut addr[suffix_offset..16])?; + + let dirty_suffix = Ipv6Addr::from(addr); + if suffix_offset < 16 { + let mask = 0xff >> (prefix % 8); + addr[suffix_offset] &= mask; + } + let suffix = Ipv6Addr::from(addr); + + let prefix_name = if data.has_remaining() { + Some(DnsPacketData::deserialize(data)?) + } else { + None + }; + + Ok(A6 { + prefix, + dirty_suffix, + suffix, + prefix_name, + }) + } +} + +impl DnsTextData for A6 { + fn dns_parse(data: &mut &str) -> ::errors::Result { + let prefix: u8 = DnsTextData::dns_parse(data) + .context("failed parsing field A6::prefix")?; + ensure!(prefix <= 128, "invalid A6::prefix {}", prefix); + + let suffix_offset = (prefix / 8) as usize; + debug_assert!(suffix_offset <= 16); + + let suffix: Ipv6Addr = DnsTextData::dns_parse(data) + .context("failed parsing field A6::suffix")?; + + // clear prefix bits + let mut suffix = suffix.octets(); + for i in 0..suffix_offset { + suffix[i] = 0; + } + if suffix_offset < 16 { + let mask = 0xff >> (prefix % 8); + suffix[suffix_offset] &= mask; + } + let suffix = Ipv6Addr::from(suffix); + + let prefix_name = if !data.is_empty() { + Some(DnsTextData::dns_parse(data) + .context("failed parsing field A6::prefix_name")?) + } else { + None + }; + + Ok(A6 { + prefix, + dirty_suffix: suffix.clone(), + suffix, + prefix_name, + }) + } + + fn dns_format(&self, f: &mut DnsTextFormatter) -> fmt::Result { + if self.dirty_suffix != self.suffix { + // parsing text clears the padding, so we would loose data. + // use binary representation instead. + return Err(fmt::Error); + } + write!(f, "{} {}", self.prefix, self.suffix)?; + if let Some(ref prefix_name) = self.prefix_name { + write!(f, "{}", prefix_name)?; + } + Ok(()) + } +} diff --git a/lib/dnsbox-base/src/ser/mod.rs b/lib/dnsbox-base/src/ser/mod.rs index 5618c56..275f7e1 100644 --- a/lib/dnsbox-base/src/ser/mod.rs +++ b/lib/dnsbox-base/src/ser/mod.rs @@ -4,4 +4,4 @@ mod rrdata; pub use self::packet::DnsPacketData; pub use self::text::DnsTextData; -pub use self::rrdata::RRData; +pub use self::rrdata::{RRDataPacket, RRData, StaticRRData}; diff --git a/lib/dnsbox-base/src/ser/packet/mod.rs b/lib/dnsbox-base/src/ser/packet/mod.rs index 496630f..f4e93e6 100644 --- a/lib/dnsbox-base/src/ser/packet/mod.rs +++ b/lib/dnsbox-base/src/ser/packet/mod.rs @@ -19,3 +19,21 @@ where } Ok(result) } + +pub fn remaining_bytes(data: &mut Cursor) -> Bytes { + let pos = data.position() as usize; + let len = data.remaining(); + let result = data.get_ref().slice(pos, pos + len); + data.advance(len); + result +} + +pub fn short_blob(data: &mut Cursor) -> Result { + check_enough_data!(data, 1, "short blob length"); + let blob_len = data.get_u8() as usize; + check_enough_data!(data, blob_len, "short blob content"); + let pos = data.position() as usize; + let blob = data.get_ref().slice(pos, pos + blob_len); + data.advance(blob_len); + Ok(blob) +} diff --git a/lib/dnsbox-base/src/ser/rrdata.rs b/lib/dnsbox-base/src/ser/rrdata.rs index c86709a..b9ea8fe 100644 --- a/lib/dnsbox-base/src/ser/rrdata.rs +++ b/lib/dnsbox-base/src/ser/rrdata.rs @@ -1,8 +1,24 @@ -use std::borrow::Cow; - +use bytes::Bytes; use common_types::Type; +use errors::*; +use std::io::Cursor; +use ser::DnsPacketData; -pub trait RRData: super::DnsPacketData { - fn rr_type() -> Type; - fn rr_type_name() -> Cow<'static, str>; +pub trait RRDataPacket: Sized { + fn deserialize_rr_data(ttl: u32, rr_class: u16, rr_type: Type, data: &mut Cursor) -> Result; +} + +impl RRDataPacket for T { + fn deserialize_rr_data(_ttl: u32, _rr_class: u16, _rr_type: Type, data: &mut Cursor) -> Result { + T::deserialize(data) + } +} + +pub trait RRData: RRDataPacket + super::DnsTextData { + fn rr_type(&self) -> Type; +} + +pub trait StaticRRData: RRData { + const TYPE: Type; + const NAME: &'static str; } diff --git a/lib/dnsbox-base/src/ser/text/mod.rs b/lib/dnsbox-base/src/ser/text/mod.rs index 4aed284..d16bfb6 100644 --- a/lib/dnsbox-base/src/ser/text/mod.rs +++ b/lib/dnsbox-base/src/ser/text/mod.rs @@ -1,4 +1,112 @@ -pub trait DnsTextData: Sized { - fn dns_parse(data: &str) -> ::errors::Result; - fn dns_format(&self) -> ::errors::Result; +use std::fmt; + +mod std_impls; +pub mod quoted; + +pub fn skip_whitespace(data: &mut &str) { + *data = (*data).trim_left(); +} + +pub fn next_field<'a>(data: &mut &'a str) -> ::errors::Result<&'a str> { + *data = (*data).trim_left(); + if data.is_empty() { bail!("missing field"); } + match data.find(char::is_whitespace) { + None => { + let result = *data; + *data = ""; + Ok(result) + }, + Some(next) => { + let result = &(*data)[..next]; + *data = &(*data)[next..].trim_left(); + Ok(result) + }, + } +} + +pub fn next_quoted_field(data: &mut &str) -> ::errors::Result> { + *data = (*data).trim_left(); + if data.is_empty() { bail!("missing field"); } + + let result = quoted::UnquoteIterator::new(data).collect::, _>>()?; + Ok(result) +} + +pub fn quote(data: &[u8]) -> String { + let mut result = String::with_capacity(data.len() + 2); + result.push('"'); + for qc in quoted::EncodeIterator::new_quoted(data) { + result += &qc; + } + result.push('"'); + result +} + +// also escapes whitespace, but doesn't use quotes to surround it +pub fn escape(data: &[u8]) -> String { + let mut result = String::with_capacity(data.len()); + for qc in quoted::EncodeIterator::new_encode_whitespace(data) { + result += &qc; + } + result +} + +/// Each call to write!() makes sure a space is emitted to separate it +/// from previous fields. +pub struct DnsTextFormatter<'a> { + f: &'a mut fmt::Formatter<'a>, + need_space: bool, +} + +impl<'a> DnsTextFormatter<'a> { + pub fn new(f: &'a mut fmt::Formatter<'a>) -> Self { + DnsTextFormatter { + f: f, + need_space: false, + } + } + + /// make sure a field separator was emitted + pub fn next_field(&mut self) -> fmt::Result { + if self.need_space { + write!(self.f, " ")?; + self.need_space = false; + } + Ok(()) + } + + /// a field was emitted through `inner`; next field needs a separator + pub fn end_field(&mut self) { + self.need_space = true; + } + + /// direct access to underlying output; you'll need to call + /// `next_field` and `end_field` manually. + pub fn inner(&mut self) -> &mut fmt::Formatter<'a> { + self.f + } + + pub fn write_fmt(&mut self, args: fmt::Arguments) -> fmt::Result { + self.next_field()?; + self.f.write_fmt(args)?; + self.end_field(); + Ok(()) + } +} + +pub trait DnsTextData: Sized { + fn dns_parse(data: &mut &str) -> ::errors::Result; + // format might fail if there is no (known) text representation. + fn dns_format(&self, f: &mut DnsTextFormatter) -> fmt::Result; +} + +pub fn parse(data: &str) -> ::errors::Result +where + T: DnsTextData +{ + let mut data = data; + let result = T::dns_parse(&mut data)?; + let data = data.trim(); + ensure!(data.is_empty(), "didn't parse complete text, remaining: {:?}", data); + Ok(result) } diff --git a/lib/dnsbox-base/src/ser/text/quoted.rs b/lib/dnsbox-base/src/ser/text/quoted.rs new file mode 100644 index 0000000..f77ce4b --- /dev/null +++ b/lib/dnsbox-base/src/ser/text/quoted.rs @@ -0,0 +1,225 @@ +use std::fmt; + +pub struct EncodedByte { + storage: [u8; 4], // max: `\000` + used: u8, +} + +impl ::std::ops::Deref for EncodedByte { + type Target = str; + + fn deref(&self) -> &Self::Target { + ::unsafe_ops::from_utf8_unchecked(&self.storage[..self.used as usize]) + } +} + +pub struct EncodeIterator<'a> { + encode_whitespace: bool, + data: &'a [u8] +} + +impl<'a> EncodeIterator<'a> { + pub fn new_quoted(value: &'a [u8]) -> Self { + EncodeIterator{ + encode_whitespace: false, + data: value, + } + } + + pub fn new_encode_whitespace(value: &'a [u8]) -> Self { + EncodeIterator{ + encode_whitespace: true, + data: value, + } + } +} + +impl<'a> Iterator for EncodeIterator<'a> { + type Item = EncodedByte; + + fn next(&mut self) -> Option { + if self.data.is_empty() { return None; } + let b = self.data[0]; + self.data = &self.data[1..]; + if b < 32 || b > 127 || (self.encode_whitespace && is_ascii_whitespace(b)) { + // `\ddd` + let d1 = b / 100; + let d2 = (b / 10) % 10; + let d3 = b % 10; + Some(EncodedByte{ + storage: [b'\\', b'0' + d1, b'0' + d2, b'0' + d3], + used: 4, + }) + } else if b == b'"' || b == b'\\' { + // `\c` + Some(EncodedByte{ + storage: [b'\\', b, 0, 0], + used: 2, + }) + } else { + Some(EncodedByte{ + storage: [b, 0, 0, 0], + used: 1, + }) + } + } +} + +#[derive(Debug)] +pub struct UnquoteError { + data: String, + position: usize, + msg: &'static str, +} + +impl fmt::Display for UnquoteError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "unquote error at position {} in {:?}: {}", self.position, self.data, self.msg) + } +} + +impl ::failure::Fail for UnquoteError {} + +pub struct UnquoteIterator<'a, 'b: 'a> { + quoted: bool, + data: &'a mut &'b str, + pos: usize, +} + +/// when walked to end without hitting errors between, the terminating +/// `"` and following whitespace will be removed from `*data`. +impl<'a, 'b: 'a> UnquoteIterator<'a, 'b> { + pub fn new(data: &'a mut &'b str) -> Self { + UnquoteIterator { + quoted: false, + data: data, + pos: 0, + } + } + + fn err(&mut self, msg: &'static str) -> Option> { + Some(Err(UnquoteError{ + data: (*self.data).into(), + position: self.pos, + msg: msg, + })) + } +} + +pub(crate) fn is_ascii_whitespace(c: u8) -> bool { + match c { + 0x09 => true, // horizontal tab: \t + 0x0a => true, // line feed: \n + 0x0c => true, // form feed: \f + 0x0d => true, // form feed: \r + 0x20 => true, // space: ' ' + _ => false, + } +} + +impl<'a, 'b: 'a> Iterator for UnquoteIterator<'a, 'b> { + type Item = Result; + + fn next(&mut self) -> Option { + let raw = self.data.as_bytes(); + + if raw.is_empty() { return self.err("empty input"); } + + if 0 == self.pos { + // check for starting quote: + if raw[0] == b'"' { + self.quoted = true; + self.pos += 1; + } + } + + if self.pos >= raw.len() { + if self.quoted { + return self.err("unexpected end of string"); + } else { + *self.data = ""; + return None; + } + } + if raw[self.pos] == b'"' { + if self.quoted { + // eat terminating quote + // pos+1 is obviously a good utf-8 boundary + *self.data = self.data[self.pos+1..].trim_left(); + return None; + } else { + return self.err("quote in the middle of unquoted string"); + } + } else if !self.quoted && is_ascii_whitespace(raw[self.pos]) { + // pos is obviously a good utf-8 boundary + *self.data = self.data[self.pos..].trim_left(); + return None; + } else if raw[self.pos] == b'\\' { + if self.pos + 1 >= raw.len() { return self.err("unexpected end of string after backslash"); } + if raw[self.pos+1] < b'0' || raw[self.pos+1] > b'9' { + let result = raw[self.pos+1]; + if !self.quoted && is_ascii_whitespace(result) { + return self.err("(escaped) whitespace not allowed in unquoted field"); + } + self.pos += 2; + return Some(Ok(result)); + } + // otherwise require 3 decimal digits + if self.pos + 3 >= raw.len() { return self.err("unexpected end of string after backslash with decimal"); } + // raw[self.pos+1] already checked for digit above + if raw[self.pos+2] < b'0' || raw[self.pos+2] > b'9' || raw[self.pos+3] < b'0' || raw[self.pos+3] > b'9' { + return self.err("expecting 3 digits after backslash with decimal"); + } + let d1 = raw[self.pos+1] - b'0'; + let d2 = raw[self.pos+2] - b'0'; + let d3 = raw[self.pos+3] - b'0'; + let val = (d1 as u32 * 100) + (d2 as u32 * 10) + (d3 as u32); + if val > 255 { return self.err("invalid decimal escape"); } + self.pos += 4; + Some(Ok(val as u8)) + } else { + let result = raw[self.pos]; + self.pos += 1; + Some(Ok(result)) + } + } +} + +#[cfg(test)] +mod tests { + use ser::text::{next_quoted_field, quote}; + + fn check_quote(data: &[u8], quoted: &str) { + assert_eq!( + quote(data), + quoted + ); + } + + fn check_unquote(mut input: &str, data: &[u8]) { + assert_eq!( + next_quoted_field(&mut input).unwrap(), + data + ); + assert!(input.is_empty()); + } + + #[test] + fn test_escapes() { + check_quote(b"\"hello \\ \xc3\xa4", r#""\"hello \\ \195\164""#); + } + + #[test] + fn test_parser() { + check_unquote(r#""\"hello \\ \195\164""#, b"\"hello \\ \xc3\xa4"); + check_unquote(r#" "\"hello \\ \195\164" "#, b"\"hello \\ \xc3\xa4"); + check_unquote(r#""\"hello \\ ä""#, b"\"hello \\ \xc3\xa4"); + check_unquote(r#" "\"hello \\ ä" "#, b"\"hello \\ \xc3\xa4"); + // unquoted input + check_unquote(r#"foobarä"#, b"foobar\xc3\xa4"); + check_unquote(r#"foobar\195\164"#, b"foobar\xc3\xa4"); + check_unquote(r#" foobarä "#, b"foobar\xc3\xa4"); + // random (unnecessary) escapes: + check_unquote(r#" "\x\%\@\." "#, b"x%@."); + } +} diff --git a/lib/dnsbox-base/src/ser/text/std_impls.rs b/lib/dnsbox-base/src/ser/text/std_impls.rs new file mode 100644 index 0000000..859fb5c --- /dev/null +++ b/lib/dnsbox-base/src/ser/text/std_impls.rs @@ -0,0 +1,100 @@ +use std::fmt; +use std::net::{Ipv4Addr, Ipv6Addr}; +use ser::text::{DnsTextData, DnsTextFormatter, next_field}; + +/* only decimal representations are used for numbers */ +impl DnsTextData for u8 { + fn dns_parse(data: &mut &str) -> ::errors::Result { + Ok(next_field(data)?.parse()?) + } + + fn dns_format(&self, f: &mut DnsTextFormatter) -> fmt::Result { + write!(f, "{}", self) + } +} + +impl DnsTextData for u16 { + fn dns_parse(data: &mut &str) -> ::errors::Result { + Ok(next_field(data)?.parse()?) + } + + fn dns_format(&self, f: &mut DnsTextFormatter) -> fmt::Result { + write!(f, "{}", self) + } +} + +impl DnsTextData for u32 { + fn dns_parse(data: &mut &str) -> ::errors::Result { + Ok(next_field(data)?.parse()?) + } + + fn dns_format(&self, f: &mut DnsTextFormatter) -> fmt::Result { + write!(f, "{}", self) + } +} + +/* only decimal representations are needed for octets */ +impl DnsTextData for Ipv4Addr { + fn dns_parse(data: &mut &str) -> ::errors::Result { + Ok(next_field(data)?.parse()?) + } + + fn dns_format(&self, f: &mut DnsTextFormatter) -> fmt::Result { + write!(f, "{}", self) + } +} + +/* representation as in RFC 3513: https://tools.ietf.org/html/rfc3513#section-2.2 */ +impl DnsTextData for Ipv6Addr { + fn dns_parse(data: &mut &str) -> ::errors::Result { + Ok(next_field(data)?.parse()?) + } + + fn dns_format(&self, f: &mut DnsTextFormatter) -> fmt::Result { + write!(f, "{}", self) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn deserialize(data: &str) -> ::errors::Result { + let mut data = data; + let res = T::dns_parse(&mut data)?; + let data = data.trim(); + ensure!(data.is_empty(), "didn't read data completely"); + Ok(res) + } + + #[test] + fn test_ipv6() { + assert_eq!( + deserialize::("FEDC:BA98:7654:3210:FEDC:BA98:7654:3210").unwrap(), + Ipv6Addr::new( + 0xfedc, 0xba98, 0x7654, 0x3210, 0xfedc, 0xba98, 0x7654, 0x3210 + ) + ); + + assert_eq!( + deserialize::("1080::8:800:200C:417A").unwrap(), + Ipv6Addr::new( + 0x1080, 0, 0, 0, 0x8, 0x800, 0x200c, 0x417a + ) + ); + + assert_eq!( + deserialize::("::13.1.68.3").unwrap(), + Ipv6Addr::new( + 0, 0, 0, 0, 0, 0, 0x0d01, 0x4403 + ) + ); + + assert_eq!( + deserialize::("::FFFF:129.144.52.38").unwrap(), + Ipv6Addr::new( + 0, 0, 0, 0, 0, 0xffff, 0x8190, 0x3426 + ) + ); + } +} diff --git a/lib/dnsbox-base/src/unsafe_ops/mod.rs b/lib/dnsbox-base/src/unsafe_ops/mod.rs new file mode 100644 index 0000000..74b4f87 --- /dev/null +++ b/lib/dnsbox-base/src/unsafe_ops/mod.rs @@ -0,0 +1,11 @@ +#[cfg(not(feature = "no-unsafe"))] +pub fn from_utf8_unchecked(v: &[u8]) -> &str { + unsafe { + ::std::str::from_utf8_unchecked(v) + } +} + +#[cfg(feature = "no-unsafe")] +pub fn from_utf8_unchecked(v: &[u8]) -> &str { + ::std::str::from_utf8(v).expect("from_utf8_unchecked") +} diff --git a/lib/dnsbox-derive/src/dns_packet_data.rs b/lib/dnsbox-derive/src/dns_packet_data.rs index dd178c3..80a7af9 100644 --- a/lib/dnsbox-derive/src/dns_packet_data.rs +++ b/lib/dnsbox-derive/src/dns_packet_data.rs @@ -1,7 +1,7 @@ use syn; use quote; -pub fn impl_hello_world(ast: &syn::DeriveInput) -> quote::Tokens { +pub fn build(ast: &syn::DeriveInput) -> quote::Tokens { let fields = match ast.body { syn::Body::Enum(_) => panic!("Deriving DnsPacketData not supported for enum types"), syn::Body::Struct(syn::VariantData::Struct(ref s)) => s, @@ -25,9 +25,10 @@ pub fn impl_hello_world(ast: &syn::DeriveInput) -> quote::Tokens { let mut parse_fields = quote!{}; for field in fields { let field_name = field.ident.as_ref().unwrap(); + let ctx_msg = format!("failed parsing field {}::{}", stringify!(#name), stringify!(#field_name)); parse_fields = quote!{#parse_fields - #field_name: DnsPacketData::deserialize(_data).with_context(|_| format!("failed parsing field {}::{}", stringify!(#name), stringify!(#field_name)))?, + #field_name: DnsPacketData::deserialize(_data).context(#ctx_msg)?, }; } diff --git a/lib/dnsbox-derive/src/dns_text_data.rs b/lib/dnsbox-derive/src/dns_text_data.rs new file mode 100644 index 0000000..8a351a7 --- /dev/null +++ b/lib/dnsbox-derive/src/dns_text_data.rs @@ -0,0 +1,56 @@ +use syn; +use quote; + +pub fn build(ast: &syn::DeriveInput) -> quote::Tokens { + let fields = match ast.body { + syn::Body::Enum(_) => panic!("Deriving DnsTextData not supported for enum types"), + syn::Body::Struct(syn::VariantData::Struct(ref s)) => s, + syn::Body::Struct(_) => panic!("Deriving DnsTextData not supported for unit / tuple struct types"), + }; + + if !ast.generics.ty_params.is_empty() { + panic!("Type parameters not supported for deriving DnsTextData"); + } + + if !ast.generics.where_clause.predicates.is_empty() { + panic!("Where clauses not supported for deriving DnsTextData"); + } + + if !ast.generics.lifetimes.is_empty() { + panic!("Lifetimes not supported for deriving DnsTextData"); + } + + let name = &ast.ident; + + let mut parse_fields = quote!{}; + let mut format_fields = quote!{}; + for field in fields { + let field_name = field.ident.as_ref().unwrap(); + + parse_fields = quote!{#parse_fields + #field_name: DnsTextData::dns_parse(_data).with_context(|_| format!("failed parsing field {}::{}", stringify!(#name), stringify!(#field_name)))?, + }; + + format_fields = quote!{#format_fields + DnsTextData::dns_format(&self.#field_name, f)?; + }; + } + + quote!{ + #[allow(unused_imports)] + impl ::dnsbox_base::ser::DnsTextData for #name { + fn dns_parse(_data: &mut &str) -> ::dnsbox_base::errors::Result { + use dnsbox_base::failure::ResultExt; + use dnsbox_base::ser::DnsTextData; + Ok(#name{ #parse_fields }) + } + + fn dns_format(&self, f: &mut ::dnsbox_base::ser::text::DnsTextFormatter) -> ::std::fmt::Result { + use dnsbox_base::ser::DnsTextData; + use std::fmt::{self, Write}; + #format_fields + Ok(()) + } + } + } +} diff --git a/lib/dnsbox-derive/src/lib.rs b/lib/dnsbox-derive/src/lib.rs index c16cf78..c0722fb 100644 --- a/lib/dnsbox-derive/src/lib.rs +++ b/lib/dnsbox-derive/src/lib.rs @@ -1,3 +1,5 @@ +#![recursion_limit="128"] + extern crate proc_macro; extern crate syn; #[macro_use] @@ -6,26 +8,56 @@ extern crate quote; use proc_macro::TokenStream; mod dns_packet_data; +mod dns_text_data; mod rrdata; +fn attr_get_single_list_arg(attr: &syn::Attribute) -> quote::Tokens { + match attr.value { + syn::MetaItem::List(_, ref l) => { + if l.len() != 1 { + panic!("{:?} attribute requires exactly one argument", attr.name()); + } + let arg = &l[0]; + quote!{#arg} + }, + syn::MetaItem::NameValue(_, ref l) => { + quote!{#l} + }, + _ => { + panic!("{:?} argument requires one argument like: [#{}(...)]", attr.name(), attr.name()); + }, + } +} + #[proc_macro_derive(DnsPacketData)] pub fn derive_dns_packet_data(input: TokenStream) -> TokenStream { let s = input.to_string(); let ast = syn::parse_derive_input(&s).unwrap(); - let gen = dns_packet_data::impl_hello_world(&ast); + let gen = dns_packet_data::build(&ast); gen.parse().unwrap() } -#[proc_macro_derive(RRData)] +#[proc_macro_derive(DnsTextData)] +pub fn derive_dns_text_data(input: TokenStream) -> TokenStream { + let s = input.to_string(); + + let ast = syn::parse_derive_input(&s).unwrap(); + + let gen = dns_text_data::build(&ast); + + gen.parse().unwrap() +} + +#[proc_macro_derive(RRData, attributes(RRTypeName))] pub fn derive_rr_data(input: TokenStream) -> TokenStream { let s = input.to_string(); let ast = syn::parse_derive_input(&s).unwrap(); - let gen = rrdata::impl_rr_data(&ast); + let gen = rrdata::build(&ast); gen.parse().unwrap() } diff --git a/lib/dnsbox-derive/src/rrdata.rs b/lib/dnsbox-derive/src/rrdata.rs index 9d52747..f83973b 100644 --- a/lib/dnsbox-derive/src/rrdata.rs +++ b/lib/dnsbox-derive/src/rrdata.rs @@ -1,7 +1,34 @@ use syn; use quote; -pub fn impl_rr_data(ast: &syn::DeriveInput) -> quote::Tokens { +use super::{attr_get_single_list_arg}; + +#[derive(Clone,Debug)] +enum StructAttribute { + RRTypeName(quote::Tokens), +} + +struct StructAttributeParser<'a>(pub &'a [syn::Attribute]); +impl<'a> Iterator for StructAttributeParser<'a> { + type Item = StructAttribute; + + fn next(&mut self) -> Option { + while !self.0.is_empty() { + let a = &self.0[0]; + self.0 = &self.0[1..]; + if a.is_sugared_doc { continue; } + match a.value.name() { + "RRTypeName" => { + return Some(StructAttribute::RRTypeName(attr_get_single_list_arg(a))); + }, + _ => (), + } + } + None + } +} + +pub fn build(ast: &syn::DeriveInput) -> quote::Tokens { if !ast.generics.ty_params.is_empty() { panic!("Type parameters not supported for deriving RRData"); } @@ -15,31 +42,44 @@ pub fn impl_rr_data(ast: &syn::DeriveInput) -> quote::Tokens { } let name = &ast.ident; - let name_str: &str = name.as_ref(); + let mut name_str = { + let name_str: &str = name.as_ref(); + quote!{#name_str} + }; + + for attr in StructAttributeParser(&ast.attrs) { + match attr { + StructAttribute::RRTypeName(name) => { + name_str = name; + }, + } + } let test_mod_name = syn::Ident::from(format!("test_rr_{}", name)); quote!{ impl ::dnsbox_base::ser::RRData for #name { - fn rr_type() -> ::dnsbox_base::common_types::Type { - ::dnsbox_base::records::types::#name + fn rr_type(&self) -> ::dnsbox_base::common_types::Type { + ::dnsbox_base::common_types::types::#name } + } - fn rr_type_name() -> ::std::borrow::Cow<'static, str> { - ::std::borrow::Cow::Borrowed(#name_str) - } + impl ::dnsbox_base::ser::StaticRRData for #name { + const TYPE: ::dnsbox_base::common_types::Type = ::dnsbox_base::common_types::types::#name; + const NAME: &'static str = #name_str; } // #[cfg(test)] #[allow(non_snake_case, unused_imports)] mod #test_mod_name { - use ::dnsbox_base::records::registry; - use ::dnsbox_base::records::types; + use dnsbox_base::records::registry; + use dnsbox_base::common_types::types; + use dnsbox_base::ser::StaticRRData; #[test] fn test_registry() { assert_eq!( - registry::known_name_to_type(#name_str), + registry::known_name_to_type(#name::NAME), Some(types::#name) ); }