use std::{
collections::BTreeMap,
fs::File,
io::{BufRead, BufReader},
ops::{Deref, DerefMut},
path::{Path, PathBuf},
};
use tracing::{debug, info};
#[cfg(feature = "dnssec")]
use crate::{
authority::DnssecAuthority,
proto::rr::dnssec::{rdata::key::KEY, DnsSecResult, SigSigner},
};
use crate::{
authority::{Authority, LookupError, LookupOptions, MessageRequest, UpdateResult, ZoneType},
proto::rr::{LowerName, Name, RecordSet, RecordType, RrKey},
proto::serialize::txt::{Lexer, Parser, Token},
server::RequestInfo,
store::{file::FileConfig, in_memory::InMemoryAuthority},
};
pub struct FileAuthority(InMemoryAuthority);
const MAX_INCLUDE_LEVEL: u16 = 256;
struct FileReaderState {
level: u16,
}
impl FileReaderState {
fn new() -> Self {
Self { level: 0 }
}
fn next_level(&self) -> Self {
Self {
level: self.level + 1,
}
}
}
impl FileAuthority {
pub fn new(
origin: Name,
records: BTreeMap<RrKey, RecordSet>,
zone_type: ZoneType,
allow_axfr: bool,
) -> Result<Self, String> {
InMemoryAuthority::new(origin, records, zone_type, allow_axfr).map(Self)
}
fn read_file(
zone_path: PathBuf,
buf: &mut String,
state: FileReaderState,
) -> Result<(), String> {
let file = File::open(&zone_path)
.map_err(|e| format!("failed to read {}: {:?}", zone_path.display(), e))?;
let reader = BufReader::new(file);
for line in reader.lines() {
let content = line.map_err(|err| format!("failed to read line: {err:?}"))?;
let mut lexer = Lexer::new(&content);
match (lexer.next_token(), lexer.next_token(), lexer.next_token()) {
(
Ok(Some(Token::Include)),
Ok(Some(Token::CharData(include_path))),
Ok(Some(Token::CharData(_domain))),
) => {
return Err(format!(
"Domain name for $INCLUDE is not supported at {}, trying to include {}",
zone_path.display(),
include_path
));
}
(Ok(Some(Token::Include)), Ok(Some(Token::CharData(include_path))), _) => {
let include_path = Path::new(&include_path);
let include_zone_path = if include_path.is_absolute() {
include_path.to_path_buf()
} else {
let parent_dir =
zone_path.parent().expect("file has to have parent folder");
parent_dir.join(include_path)
};
if state.level >= MAX_INCLUDE_LEVEL {
return Err(format!("Max depth level for nested $INCLUDE is reached at {}, trying to include {}", zone_path.display(), include_zone_path.display()));
}
let mut include_buf = String::new();
info!(
"including file {} into {}",
include_zone_path.display(),
zone_path.display()
);
Self::read_file(include_zone_path, &mut include_buf, state.next_level())?;
buf.push_str(&include_buf);
}
_ => {
buf.push_str(&content);
}
}
buf.push('\n');
}
Ok(())
}
pub fn try_from_config(
origin: Name,
zone_type: ZoneType,
allow_axfr: bool,
root_dir: Option<&Path>,
config: &FileConfig,
) -> Result<Self, String> {
let root_dir_path = root_dir.map(PathBuf::from).unwrap_or_else(PathBuf::new);
let zone_path = root_dir_path.join(&config.zone_file_path);
info!("loading zone file: {:?}", zone_path);
let mut buf = String::new();
Self::read_file(zone_path, &mut buf, FileReaderState::new())
.map_err(|e| format!("failed to read {}: {:?}", &config.zone_file_path, e))?;
let lexer = Lexer::new(&buf);
let (origin, records) = Parser::new()
.parse(lexer, Some(origin))
.map_err(|e| format!("failed to parse {}: {:?}", config.zone_file_path, e))?;
info!(
"zone file loaded: {} with {} records",
origin,
records.len()
);
debug!("zone: {:#?}", records);
Self::new(origin, records, zone_type, allow_axfr)
}
pub fn unwrap(self) -> InMemoryAuthority {
self.0
}
}
impl Deref for FileAuthority {
type Target = InMemoryAuthority;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for FileAuthority {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
#[async_trait::async_trait]
impl Authority for FileAuthority {
type Lookup = <InMemoryAuthority as Authority>::Lookup;
fn zone_type(&self) -> ZoneType {
self.0.zone_type()
}
fn is_axfr_allowed(&self) -> bool {
self.0.is_axfr_allowed()
}
async fn update(&self, _update: &MessageRequest) -> UpdateResult<bool> {
use crate::proto::op::ResponseCode;
Err(ResponseCode::NotImp)
}
fn origin(&self) -> &LowerName {
self.0.origin()
}
async fn lookup(
&self,
name: &LowerName,
rtype: RecordType,
lookup_options: LookupOptions,
) -> Result<Self::Lookup, LookupError> {
self.0.lookup(name, rtype, lookup_options).await
}
async fn search(
&self,
request_info: RequestInfo<'_>,
lookup_options: LookupOptions,
) -> Result<Self::Lookup, LookupError> {
self.0.search(request_info, lookup_options).await
}
async fn ns(&self, lookup_options: LookupOptions) -> Result<Self::Lookup, LookupError> {
self.0.ns(lookup_options).await
}
async fn get_nsec_records(
&self,
name: &LowerName,
lookup_options: LookupOptions,
) -> Result<Self::Lookup, LookupError> {
self.0.get_nsec_records(name, lookup_options).await
}
async fn soa(&self) -> Result<Self::Lookup, LookupError> {
self.0.soa().await
}
async fn soa_secure(&self, lookup_options: LookupOptions) -> Result<Self::Lookup, LookupError> {
self.0.soa_secure(lookup_options).await
}
}
#[cfg(feature = "dnssec")]
#[cfg_attr(docsrs, doc(cfg(feature = "dnssec")))]
#[async_trait::async_trait]
impl DnssecAuthority for FileAuthority {
async fn add_update_auth_key(&self, name: Name, key: KEY) -> DnsSecResult<()> {
self.0.add_update_auth_key(name, key).await
}
async fn add_zone_signing_key(&self, signer: SigSigner) -> DnsSecResult<()> {
self.0.add_zone_signing_key(signer).await
}
async fn secure_zone(&self) -> DnsSecResult<()> {
DnssecAuthority::secure_zone(&self.0).await
}
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use crate::proto::rr::{rdata::A, RData};
use futures_executor::block_on;
use super::*;
use crate::authority::ZoneType;
#[test]
fn test_load_zone() {
#[cfg(feature = "dnssec")]
let config = FileConfig {
zone_file_path: "../../tests/test-data/test_configs/dnssec/example.com.zone"
.to_string(),
};
#[cfg(not(feature = "dnssec"))]
let config = FileConfig {
zone_file_path: "../../tests/test-data/test_configs/example.com.zone".to_string(),
};
let authority = FileAuthority::try_from_config(
Name::from_str("example.com.").unwrap(),
ZoneType::Primary,
false,
None,
&config,
)
.expect("failed to load file");
let lookup = block_on(Authority::lookup(
&authority,
&LowerName::from_str("www.example.com.").unwrap(),
RecordType::A,
LookupOptions::default(),
))
.expect("lookup failed");
match lookup
.into_iter()
.next()
.expect("A record not found in authority")
.data()
{
Some(RData::A(ip)) => assert_eq!(A::new(127, 0, 0, 1), *ip),
_ => panic!("wrong rdata type returned"),
}
let include_lookup = block_on(Authority::lookup(
&authority,
&LowerName::from_str("include.alias.example.com.").unwrap(),
RecordType::A,
LookupOptions::default(),
))
.expect("INCLUDE lookup failed");
match include_lookup
.into_iter()
.next()
.expect("A record not found in authority")
.data()
{
Some(RData::A(ip)) => assert_eq!(A::new(127, 0, 0, 5), *ip),
_ => panic!("wrong rdata type returned"),
}
}
}