use crate::bail;
use crate::io::{CursorExt, DNSReadExt, SeekExt};
use crate::types::*;
use crate::ParseError;
use byteorder::{ReadBytesExt, BE};
use std::io;
use std::io::Cursor;
use std::io::Read;
use std::net::{Ipv4Addr, Ipv6Addr};
use std::time::Duration;
pub type A = Ipv4Addr;
#[allow(clippy::upper_case_acronyms)]
pub type AAAA = Ipv6Addr;
pub type NS = String;
#[allow(clippy::upper_case_acronyms)]
pub type CNAME = String;
#[allow(clippy::upper_case_acronyms)]
pub type PTR = String;
#[allow(clippy::upper_case_acronyms)]
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct TXT(pub Vec<Vec<u8>>);
impl Record {
pub(crate) fn parse(
cur: &mut Cursor<&[u8]>,
name: String,
r#type: Type,
class: Class,
) -> io::Result<Record> {
let ttl = cur.read_u32::<BE>()?;
let len = cur.read_u16::<BE>()?;
let pos = cur.position();
let end = pos as usize + len as usize;
let mut record = cur.sub_cursor(0, end)?;
record.set_position(pos);
let resource = match r#type {
Type::A => Resource::A(parse_a(&mut record, class)?),
Type::AAAA => Resource::AAAA(parse_aaaa(&mut record, class)?),
Type::NS => Resource::NS(record.read_qname()?),
Type::SOA => Resource::SOA(SOA::parse(&mut record)?),
Type::CNAME => Resource::CNAME(record.read_qname()?),
Type::PTR => Resource::PTR(record.read_qname()?),
Type::MX => Resource::MX(MX::parse(&mut record)?),
Type::TXT => Resource::TXT(parse_txt(&mut record)?),
Type::SPF => Resource::SPF(parse_txt(&mut record)?),
Type::SRV => Resource::SRV(SRV::parse(&mut record)?),
Type::Reserved | Type::OPT | Type::ANY => {
bail!(InvalidData, "invalid record type '{}'", r#type);
}
};
if record.remaining()? > 0 {
bail!(
Other,
"finished '{}' parsing record with {} bytes left over",
r#type,
record.remaining()?
);
}
cur.set_position(record.position());
Ok(Record {
name,
class,
ttl: Duration::from_secs(ttl.into()),
resource,
})
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct MX {
pub preference: u16,
pub exchange: String,
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
#[allow(clippy::upper_case_acronyms)]
pub struct SOA {
pub mname: String,
pub rname: String,
pub serial: u32,
pub refresh: Duration,
pub retry: Duration,
pub expire: Duration,
pub minimum: Duration,
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
#[allow(clippy::upper_case_acronyms)]
pub struct SRV {
pub priority: u16,
pub weight: u16,
pub port: u16,
pub name: String,
}
fn parse_a(cur: &mut Cursor<&[u8]>, class: Class) -> io::Result<A> {
let mut buf = [0_u8; 4];
cur.read_exact(&mut buf)?;
match class {
Class::Internet => Ok(A::new(buf[0], buf[1], buf[2], buf[3])),
_ => bail!(InvalidData, "unsupported A record class '{}'", class),
}
}
fn parse_aaaa(cur: &mut Cursor<&[u8]>, class: Class) -> io::Result<AAAA> {
let mut buf = [0_u8; 16];
cur.read_exact(&mut buf)?;
match class {
Class::Internet => Ok(AAAA::from(buf)),
_ => bail!(InvalidData, "unsupported AAAA record class '{}'", class),
}
}
fn parse_txt(cur: &mut Cursor<&[u8]>) -> io::Result<TXT> {
let mut txts = Vec::new();
loop {
let len = match cur.read_u8() {
Ok(len) => len,
Err(e) => match e.kind() {
io::ErrorKind::UnexpectedEof => break,
_ => return Err(e),
},
};
let mut txt = vec![0; len.into()];
cur.read_exact(&mut txt)?;
txts.push(txt)
}
Ok(TXT(txts))
}
impl SOA {
pub(crate) fn parse(cur: &mut Cursor<&[u8]>) -> io::Result<SOA> {
let mname = cur.read_qname()?;
let rname = Self::rname_to_email(&cur.read_qname()?).unwrap();
let serial = cur.read_u32::<BE>()?;
let refresh = cur.read_u32::<BE>()?;
let retry = cur.read_u32::<BE>()?;
let expire = cur.read_u32::<BE>()?;
let minimum = cur.read_u32::<BE>()?;
Ok(SOA {
mname,
rname,
serial,
refresh: Duration::from_secs(refresh.into()),
retry: Duration::from_secs(retry.into()),
expire: Duration::from_secs(expire.into()),
minimum: Duration::from_secs(minimum.into()),
})
}
pub fn rname_to_email(domain: &str) -> Result<String, ParseError> {
let mut result = String::with_capacity(domain.len());
let mut last_char = ' ';
let mut done = false;
for c in domain.chars() {
if last_char == '\\' {
result.push(c);
} else if c == '.' && !done {
result.push('@');
done = true;
} else if c != '\\' {
result.push(c);
}
last_char = c;
}
if !done {
return Err(ParseError::InvalidRname(domain.to_string()));
}
Ok(result)
}
pub fn email_to_rname(email: &str) -> Result<String, ParseError> {
match email.split_once('@') {
None => Err(ParseError::InvalidRname(email.to_string())),
Some((left, right)) => Ok(left.replace('.', "\\.") + "." + right),
}
}
}
impl MX {
pub(crate) fn parse(cur: &mut Cursor<&[u8]>) -> io::Result<MX> {
let preference = cur.read_u16::<BE>()?;
let exchange = cur.read_qname()?;
Ok(MX {
preference,
exchange,
})
}
}
impl SRV {
pub(crate) fn parse(cur: &mut Cursor<&[u8]>) -> io::Result<SRV> {
let priority = cur.read_u16::<BE>()?;
let weight = cur.read_u16::<BE>()?;
let port = cur.read_u16::<BE>()?;
let name = cur.read_qname()?;
Ok(SRV {
priority,
weight,
port,
name,
})
}
}
impl From<&str> for TXT {
fn from(txt: &str) -> TXT {
TXT(vec![txt.as_bytes().to_vec()])
}
}
impl From<&[&str]> for TXT {
fn from(txts: &[&str]) -> TXT {
TXT(txts.iter().map(|row| row.as_bytes().to_vec()).collect())
}
}
#[cfg(test)]
mod tests {
use crate::SOA;
use pretty_assertions::assert_eq;
static RNAME_TESTS: &[(&str, &str)] = &[
("username.example.com", "username@example.com"),
("root.localhost", "root@localhost"),
("Action\\.domains.ISI.EDU", "Action.domains@ISI.EDU"),
("a\\.b\\.c.ISI.EDU", "a.b.c@ISI.EDU"),
];
#[test]
fn test_soa_rname_to_email() {
for (domain, email) in RNAME_TESTS {
match SOA::rname_to_email(domain) {
Ok(got) => assert_eq!(got, *email, "incorrect result for '{}'", domain),
Err(err) => panic!("'{}' Failed:\n{:?}", domain, err),
}
}
}
#[test]
fn test_soa_rname_from_email() {
for (domain, email) in RNAME_TESTS {
match SOA::email_to_rname(email) {
Ok(got) => assert_eq!(got, *domain, "incorrect result for '{}'", email),
Err(err) => panic!("'{}' Failed:\n{:?}", email, err),
}
}
}
}