use crate::rndc_conf_types::{KeyBlock, OptionsBlock, RndcConfFile, ServerAddress, ServerBlock};
use nom::{
branch::alt,
bytes::complete::{tag, take_until, take_while, take_while1},
character::complete::{char, digit1, multispace0, multispace1},
combinator::{map, map_res, recognize, value},
multi::{many0, separated_list0},
sequence::{delimited, preceded},
IResult, Parser,
};
use std::collections::HashSet;
use std::net::IpAddr;
use std::path::{Path, PathBuf};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum RndcConfParseError {
#[error("Parse error: {0}")]
ParseError(String),
#[error("Invalid server address: {0}")]
InvalidServerAddress(String),
#[error("Invalid IP address: {0}")]
InvalidIpAddress(String),
#[error("Missing required field: {0}")]
MissingField(String),
#[error("Circular include detected: {0}")]
CircularInclude(String),
#[error("File not found: {0}")]
FileNotFound(String),
#[error("IO error: {0}")]
IoError(#[from] std::io::Error),
#[error("Incomplete input")]
Incomplete,
}
pub type ParseResult<T> = Result<T, RndcConfParseError>;
fn line_comment(input: &str) -> IResult<&str, ()> {
let (input, _) = tag("//")(input)?;
let (input, _) = take_while(|c| c != '\n')(input)?;
Ok((input, ()))
}
fn hash_comment(input: &str) -> IResult<&str, ()> {
let (input, _) = char('#')(input)?;
let (input, _) = take_while(|c| c != '\n')(input)?;
Ok((input, ()))
}
fn block_comment(input: &str) -> IResult<&str, ()> {
value((), (tag("/*"), take_until("*/"), tag("*/"))).parse(input)
}
fn comment(input: &str) -> IResult<&str, ()> {
alt((line_comment, hash_comment, block_comment)).parse(input)
}
fn ws<'a, F, O>(inner: F) -> impl Parser<&'a str, Output = O, Error = nom::error::Error<&'a str>>
where
F: Parser<&'a str, Output = O, Error = nom::error::Error<&'a str>>,
{
delimited(
many0(alt((value((), multispace1), comment))),
inner,
many0(alt((value((), multispace1), comment))),
)
}
fn semicolon(input: &str) -> IResult<&str, char> {
ws(char(';')).parse(input)
}
fn escaped_char(input: &str) -> IResult<&str, char> {
preceded(
char('\\'),
alt((
value('"', char('"')),
value('\\', char('\\')),
value('\n', char('n')),
value('\r', char('r')),
value('\t', char('t')),
)),
)
.parse(input)
}
fn quoted_string(input: &str) -> IResult<&str, String> {
delimited(
char('"'),
map(
many0(alt((
map(escaped_char, |c| c.to_string()),
map(take_while1(|c| c != '"' && c != '\\'), |s: &str| {
s.to_string()
}),
))),
|parts| parts.join(""),
),
char('"'),
)
.parse(input)
}
fn identifier(input: &str) -> IResult<&str, &str> {
take_while1(|c: char| c.is_alphanumeric() || c == '_' || c == '-' || c == '.' || c == ':')(
input,
)
}
fn ipv4_addr(input: &str) -> IResult<&str, IpAddr> {
let (input, addr_str) = recognize((
digit1,
char('.'),
digit1,
char('.'),
digit1,
char('.'),
digit1,
))
.parse(input)?;
let addr = match addr_str.parse::<IpAddr>() {
Ok(addr) => addr,
Err(_) => {
return Err(nom::Err::Error(nom::error::Error::new(
input,
nom::error::ErrorKind::Verify,
)))
}
};
Ok((input, addr))
}
fn ipv6_addr(input: &str) -> IResult<&str, IpAddr> {
let (input, addr_str) =
recognize(take_while1(|c: char| c.is_ascii_hexdigit() || c == ':')).parse(input)?;
if !addr_str.contains("::") && addr_str.matches(':').count() < 2 {
return Err(nom::Err::Error(nom::error::Error::new(
input,
nom::error::ErrorKind::Verify,
)));
}
let addr = match addr_str.parse::<IpAddr>() {
Ok(addr) => addr,
Err(_) => {
return Err(nom::Err::Error(nom::error::Error::new(
input,
nom::error::ErrorKind::Verify,
)))
}
};
Ok((input, addr))
}
fn ip_addr(input: &str) -> IResult<&str, IpAddr> {
alt((ipv6_addr, ipv4_addr)).parse(input)
}
fn port_number(input: &str) -> IResult<&str, u16> {
map_res(digit1, |s: &str| s.parse::<u16>()).parse(input)
}
fn server_address(input: &str) -> IResult<&str, ServerAddress> {
alt((
map(ip_addr, ServerAddress::IpAddr),
map(identifier, |s: &str| ServerAddress::Hostname(s.to_string())),
))
.parse(input)
}
enum KeyField {
Algorithm(String),
Secret(String),
}
impl std::fmt::Debug for KeyField {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
KeyField::Algorithm(a) => f.debug_tuple("Algorithm").field(a).finish(),
KeyField::Secret(_) => f.debug_tuple("Secret").field(&"[REDACTED]").finish(),
}
}
}
fn parse_algorithm_field(input: &str) -> IResult<&str, KeyField> {
let (input, _) = ws(tag("algorithm")).parse(input)?;
let (input, algo) = ws(identifier).parse(input)?;
let (input, _) = semicolon(input)?;
Ok((input, KeyField::Algorithm(algo.to_string())))
}
fn parse_secret_field(input: &str) -> IResult<&str, KeyField> {
let (input, _) = ws(tag("secret")).parse(input)?;
let (input, secret) = ws(quoted_string).parse(input)?;
let (input, _) = semicolon(input)?;
Ok((input, KeyField::Secret(secret)))
}
fn parse_key_field(input: &str) -> IResult<&str, KeyField> {
alt((parse_algorithm_field, parse_secret_field)).parse(input)
}
fn parse_key_block(input: &str) -> IResult<&str, (String, KeyBlock)> {
let (input, _) = ws(tag("key")).parse(input)?;
let (input, name) = ws(quoted_string).parse(input)?;
let (input, fields) =
delimited(ws(char('{')), many0(parse_key_field), ws(tag("};"))).parse(input)?;
let mut algorithm = None;
let mut secret = None;
for field in fields {
match field {
KeyField::Algorithm(a) => algorithm = Some(a),
KeyField::Secret(s) => secret = Some(s),
}
}
let key_block = KeyBlock {
name: name.clone(),
algorithm: algorithm.unwrap_or_else(|| "hmac-sha256".to_string()),
secret: secret.unwrap_or_default(),
};
Ok((input, (name, key_block)))
}
#[derive(Debug)]
enum ServerField {
Key(String),
Port(u16),
Addresses(Vec<IpAddr>),
}
fn parse_server_key_field(input: &str) -> IResult<&str, ServerField> {
let (input, _) = ws(tag("key")).parse(input)?;
let (input, key) = ws(quoted_string).parse(input)?;
let (input, _) = semicolon(input)?;
Ok((input, ServerField::Key(key)))
}
fn parse_server_port_field(input: &str) -> IResult<&str, ServerField> {
let (input, _) = ws(tag("port")).parse(input)?;
let (input, port) = ws(port_number).parse(input)?;
let (input, _) = semicolon(input)?;
Ok((input, ServerField::Port(port)))
}
fn parse_server_addresses_field(input: &str) -> IResult<&str, ServerField> {
let (input, _) = ws(tag("addresses")).parse(input)?;
let (input, addrs) = delimited(
ws(char('{')),
separated_list0(semicolon, ws(ip_addr)),
ws(tag("};")),
)
.parse(input)?;
Ok((input, ServerField::Addresses(addrs)))
}
fn parse_server_field(input: &str) -> IResult<&str, ServerField> {
alt((
parse_server_key_field,
parse_server_port_field,
parse_server_addresses_field,
))
.parse(input)
}
fn parse_server_block(input: &str) -> IResult<&str, (String, ServerBlock)> {
let (input, _) = ws(tag("server")).parse(input)?;
let (input, addr) = ws(server_address).parse(input)?;
let (input, fields) =
delimited(ws(char('{')), many0(parse_server_field), ws(tag("};"))).parse(input)?;
let mut server = ServerBlock::new(addr.clone());
for field in fields {
match field {
ServerField::Key(k) => server.key = Some(k),
ServerField::Port(p) => server.port = Some(p),
ServerField::Addresses(a) => server.addresses = Some(a),
}
}
Ok((input, (addr.to_string(), server)))
}
#[derive(Debug)]
#[allow(clippy::enum_variant_names)]
enum OptionField {
DefaultServer(String),
DefaultKey(String),
DefaultPort(u16),
}
fn parse_default_server_field(input: &str) -> IResult<&str, OptionField> {
let (input, _) = ws(tag("default-server")).parse(input)?;
let (input, server) = ws(identifier).parse(input)?;
let (input, _) = semicolon(input)?;
Ok((input, OptionField::DefaultServer(server.to_string())))
}
fn parse_default_key_field(input: &str) -> IResult<&str, OptionField> {
let (input, _) = ws(tag("default-key")).parse(input)?;
let (input, key) = ws(quoted_string).parse(input)?;
let (input, _) = semicolon(input)?;
Ok((input, OptionField::DefaultKey(key)))
}
fn parse_default_port_field(input: &str) -> IResult<&str, OptionField> {
let (input, _) = ws(tag("default-port")).parse(input)?;
let (input, port) = ws(port_number).parse(input)?;
let (input, _) = semicolon(input)?;
Ok((input, OptionField::DefaultPort(port)))
}
fn parse_option_field(input: &str) -> IResult<&str, OptionField> {
alt((
parse_default_server_field,
parse_default_key_field,
parse_default_port_field,
))
.parse(input)
}
fn parse_options_block(input: &str) -> IResult<&str, OptionsBlock> {
let (input, _) = ws(tag("options")).parse(input)?;
let (input, fields) =
delimited(ws(char('{')), many0(parse_option_field), ws(tag("};"))).parse(input)?;
let mut options = OptionsBlock::new();
for field in fields {
match field {
OptionField::DefaultServer(s) => options.default_server = Some(s),
OptionField::DefaultKey(k) => options.default_key = Some(k),
OptionField::DefaultPort(p) => options.default_port = Some(p),
}
}
Ok((input, options))
}
fn parse_include_stmt(input: &str) -> IResult<&str, PathBuf> {
let (input, _) = ws(tag("include")).parse(input)?;
let (input, path) = ws(quoted_string).parse(input)?;
let (input, _) = semicolon(input)?;
Ok((input, PathBuf::from(path)))
}
#[derive(Debug)]
enum Statement {
Include(PathBuf),
Key(String, KeyBlock),
Server(String, ServerBlock),
Options(OptionsBlock),
}
fn parse_statement(input: &str) -> IResult<&str, Statement> {
alt((
map(parse_include_stmt, Statement::Include),
map(parse_key_block, |(name, key)| Statement::Key(name, key)),
map(parse_server_block, |(addr, srv)| {
Statement::Server(addr, srv)
}),
map(parse_options_block, Statement::Options),
))
.parse(input)
}
fn parse_rndc_conf_internal(input: &str) -> IResult<&str, RndcConfFile> {
let (input, statements) = many0(ws(parse_statement)).parse(input)?;
let (input, _) = multispace0(input)?;
let mut conf = RndcConfFile::new();
for stmt in statements {
match stmt {
Statement::Include(path) => conf.includes.push(path),
Statement::Key(name, key) => {
conf.keys.insert(name, key);
}
Statement::Server(addr, server) => {
conf.servers.insert(addr, server);
}
Statement::Options(opts) => {
if opts.default_server.is_some() {
conf.options.default_server = opts.default_server;
}
if opts.default_key.is_some() {
conf.options.default_key = opts.default_key;
}
if opts.default_port.is_some() {
conf.options.default_port = opts.default_port;
}
}
}
}
Ok((input, conf))
}
pub fn parse_rndc_conf_str(input: &str) -> ParseResult<RndcConfFile> {
match parse_rndc_conf_internal(input) {
Ok((remaining, conf)) => {
if !remaining.trim().is_empty() {
let offset = input.len().saturating_sub(remaining.len());
return Err(RndcConfParseError::ParseError(format!(
"malformed rndc.conf: unparsed input near byte {}",
offset
)));
}
Ok(conf)
}
Err(nom::Err::Error(e)) | Err(nom::Err::Failure(e)) => {
let offset = input.len().saturating_sub(e.input.len());
Err(RndcConfParseError::ParseError(format!(
"malformed rndc.conf near byte {} ({:?})",
offset, e.code
)))
}
Err(nom::Err::Incomplete(_)) => Err(RndcConfParseError::Incomplete),
}
}
pub fn parse_rndc_conf_file(path: &Path) -> ParseResult<RndcConfFile> {
let mut visited = HashSet::new();
parse_rndc_conf_file_recursive(path, &mut visited, 0)
}
const MAX_INCLUDE_DEPTH: usize = 32;
fn parse_rndc_conf_file_recursive(
path: &Path,
visited: &mut HashSet<PathBuf>,
depth: usize,
) -> ParseResult<RndcConfFile> {
if depth > MAX_INCLUDE_DEPTH {
return Err(RndcConfParseError::ParseError(format!(
"include nesting exceeds maximum depth of {}",
MAX_INCLUDE_DEPTH
)));
}
let canonical_path = path
.canonicalize()
.map_err(|_| RndcConfParseError::FileNotFound(path.display().to_string()))?;
if visited.contains(&canonical_path) {
return Err(RndcConfParseError::CircularInclude(
canonical_path.display().to_string(),
));
}
visited.insert(canonical_path.clone());
let content = std::fs::read_to_string(path)?;
let mut conf = parse_rndc_conf_str(&content)?;
let includes = conf.includes.clone();
conf.includes.clear();
for include_path in includes {
let resolved_path = if include_path.is_absolute() {
include_path
} else {
path.parent()
.unwrap_or_else(|| Path::new("."))
.join(include_path)
};
let included_conf = parse_rndc_conf_file_recursive(&resolved_path, visited, depth + 1)?;
for (name, key) in included_conf.keys {
conf.keys.entry(name).or_insert(key);
}
for (addr, server) in included_conf.servers {
conf.servers.entry(addr).or_insert(server);
}
if conf.options.default_server.is_none() {
conf.options.default_server = included_conf.options.default_server;
}
if conf.options.default_key.is_none() {
conf.options.default_key = included_conf.options.default_key;
}
if conf.options.default_port.is_none() {
conf.options.default_port = included_conf.options.default_port;
}
conf.includes.push(resolved_path);
}
Ok(conf)
}
#[cfg(test)]
#[path = "rndc_conf_parser_tests.rs"]
mod tests;