use crate::{dsl::directives::Directives, RuleEngine};
use anyhow::Context;
const DEFAULT_ROOT_FILTERING_RULES: &str = include_str!("../api/default/root_filter_rules.rhai");
const DEFAULT_FALLBACK_RULES: &str = include_str!("../api/default/fallback_rules.rhai");
const DEFAULT_INCOMING_RULES: &str = include_str!("../api/default/incoming_rules.rhai");
const DEFAULT_OUTGOING_RULES: &str = include_str!("../api/default/outgoing_rules.rhai");
const DEFAULT_INTERNAL_RULES: &str = include_str!("../api/default/internal_rules.rhai");
#[derive(Debug)]
pub struct SubDomainHierarchy {
pub root_filter: Script,
pub fallback: Script,
pub domains: std::collections::BTreeMap<String, DomainDirectives>,
}
#[derive(Debug)]
pub struct DomainDirectives {
pub incoming: Script,
pub outgoing: Script,
pub internal: Script,
}
#[derive(Debug)]
pub struct Script {
pub ast: rhai::AST,
pub directives: Directives,
}
impl SubDomainHierarchy {
#[tracing::instrument(skip(engine), err)]
pub fn new(
engine: &rhai::Engine,
filter_path: &std::path::Path,
domain_dir: Option<&std::path::Path>,
) -> anyhow::Result<Self> {
let mut hierarchy = std::collections::BTreeMap::new();
tracing::debug!(
"Expecting '{:?}/**/{{incoming,outgoing,internal}}.vsl'",
domain_dir
);
if let Some(domain_dir) = domain_dir {
for entry in std::fs::read_dir(domain_dir).with_context(|| {
format!("Cannot read domain directory in '{}'", domain_dir.display())
})? {
let entry = entry?;
if entry.file_type()?.is_file() {
continue;
}
let domain_dir = entry.path();
let domain = domain_dir
.file_name()
.and_then(std::ffi::OsStr::to_str)
.ok_or_else(|| anyhow::anyhow!("failed to get file name"))?;
hierarchy.insert(
domain.to_owned(),
DomainDirectives {
incoming: Self::rules_from_path_or_default(
engine,
&domain_dir.join("incoming.vsl"),
DEFAULT_INCOMING_RULES,
)
.with_context(|| {
format!(
"failed to compile the 'incoming.vsl' script for the '{domain}' domain"
)
})?,
outgoing: Self::rules_from_path_or_default(
engine,
&domain_dir.join("outgoing.vsl"),
DEFAULT_OUTGOING_RULES,
)
.with_context(|| {
format!(
"failed to compile the 'outgoing.vsl' script for the '{domain}' domain"
)
})?,
internal: Self::rules_from_path_or_default(
engine,
&domain_dir.join("internal.vsl"),
DEFAULT_INTERNAL_RULES,
)
.with_context(|| {
format!(
"failed to compile the 'internal.vsl' script for the '{domain}' domain"
)
})?,
},
);
}
}
Ok(Self {
root_filter: Self::rules_from_path_or_default(
engine,
filter_path,
DEFAULT_ROOT_FILTERING_RULES,
)
.context("failed to load your root filtering script (filter.vsl)")?,
fallback: Self::rules_from_script(engine, DEFAULT_FALLBACK_RULES)
.context("failed to load fallback rules: this is a bug, please report it.")?,
domains: hierarchy,
})
}
pub fn new_empty(engine: &rhai::Engine) -> anyhow::Result<Self> {
Ok(Self {
root_filter: Self::rules_from_script(engine, DEFAULT_ROOT_FILTERING_RULES)?,
fallback: Self::rules_from_script(engine, DEFAULT_FALLBACK_RULES)?,
domains: std::collections::BTreeMap::new(),
})
}
#[tracing::instrument(skip(engine, default), err)]
fn rules_from_path_or_default(
engine: &rhai::Engine,
path: &std::path::Path,
default: &str,
) -> anyhow::Result<Script> {
let source = std::fs::read_to_string(path).unwrap_or_else(|error| {
tracing::warn!(
%error, "script at {path:?} could not be loaded, using default rules instead"
);
default.to_string()
});
Self::rules_from_script(engine, &source)
}
fn rules_from_script(engine: &rhai::Engine, script: &str) -> anyhow::Result<Script> {
tracing::debug!("Compiling {script:?}...");
let ast = engine
.compile_into_self_contained(&rhai::Scope::new(), script)
.map_err(|err| anyhow::anyhow!("failed to compile vsl scripts: {err}"))?;
let directives = RuleEngine::extract_directives(engine, &ast)?;
Ok(Script { ast, directives })
}
}
#[derive(Debug)]
pub struct Builder<'a> {
engine: &'a rhai::Engine,
inner: SubDomainHierarchy,
}
impl<'a> Builder<'a> {
pub fn new(engine: &'a rhai::Engine) -> anyhow::Result<Self> {
Ok(Self {
engine,
inner: SubDomainHierarchy::new_empty(engine)?,
})
}
pub fn add_root_filter_rules(mut self, script: &str) -> anyhow::Result<Self> {
self.inner.root_filter = SubDomainHierarchy::rules_from_script(self.engine, script)?;
Ok(self)
}
pub fn add_domain_rules(
self,
domain: impl Into<String>,
) -> DomainDirectivesBuilder<'a, WantsIncoming> {
DomainDirectivesBuilder {
inner: self,
domain: domain.into(),
state: WantsIncoming {},
}
}
#[allow(clippy::missing_const_for_fn)] #[must_use]
pub fn build(self) -> SubDomainHierarchy {
self.inner
}
}
#[derive(Debug)]
pub struct DomainDirectivesBuilder<'a, State: std::fmt::Debug> {
inner: Builder<'a>,
domain: String,
state: State,
}
#[derive(Debug)]
pub struct WantsIncoming;
impl<'a> DomainDirectivesBuilder<'a, WantsIncoming> {
pub fn with_incoming(
self,
incoming: &str,
) -> anyhow::Result<DomainDirectivesBuilder<'a, WantsOutgoing>> {
Ok(DomainDirectivesBuilder::<'a, WantsOutgoing> {
state: WantsOutgoing {
incoming: SubDomainHierarchy::rules_from_script(self.inner.engine, incoming)?,
},
inner: self.inner,
domain: self.domain,
})
}
pub fn with_default(self) -> anyhow::Result<DomainDirectivesBuilder<'a, WantsOutgoing>> {
self.with_incoming(DEFAULT_INCOMING_RULES)
}
}
#[derive(Debug)]
pub struct WantsOutgoing {
incoming: Script,
}
impl<'a> DomainDirectivesBuilder<'a, WantsOutgoing> {
pub fn with_outgoing(
self,
outgoing: &str,
) -> anyhow::Result<DomainDirectivesBuilder<'a, WantsInternal>> {
Ok(DomainDirectivesBuilder::<'a, WantsInternal> {
state: WantsInternal {
parent: self.state,
outgoing: SubDomainHierarchy::rules_from_script(self.inner.engine, outgoing)?,
},
inner: self.inner,
domain: self.domain,
})
}
pub fn with_default(self) -> anyhow::Result<DomainDirectivesBuilder<'a, WantsInternal>> {
self.with_outgoing(DEFAULT_OUTGOING_RULES)
}
}
#[derive(Debug)]
pub struct WantsInternal {
parent: WantsOutgoing,
outgoing: Script,
}
impl<'a> DomainDirectivesBuilder<'a, WantsInternal> {
pub fn with_internal(
self,
internal: &str,
) -> anyhow::Result<DomainDirectivesBuilder<'a, WantsBuild>> {
Ok(DomainDirectivesBuilder::<'a, WantsBuild> {
state: WantsBuild {
parent: self.state,
internal: SubDomainHierarchy::rules_from_script(self.inner.engine, internal)?,
},
inner: self.inner,
domain: self.domain,
})
}
pub fn with_default(self) -> anyhow::Result<DomainDirectivesBuilder<'a, WantsBuild>> {
self.with_internal(DEFAULT_INTERNAL_RULES)
}
}
#[derive(Debug)]
pub struct WantsBuild {
parent: WantsInternal,
internal: Script,
}
impl<'a> DomainDirectivesBuilder<'a, WantsBuild> {
#[allow(clippy::missing_const_for_fn)] #[must_use]
pub fn build(mut self) -> Builder<'a> {
self.inner.inner.domains.insert(
self.domain,
DomainDirectives {
incoming: self.state.parent.parent.incoming,
outgoing: self.state.parent.outgoing,
internal: self.state.internal,
},
);
self.inner
}
}