use std::path::PathBuf;
#[derive(knus::Decode, Debug)]
pub struct Config {
#[knus(child, unwrap(argument))]
pub compression: Option<bool>,
#[knus(child)]
pub rate_limit: Option<RateLimitNode>,
#[knus(child)]
pub cache: Option<CacheNode>,
#[knus(children(name = "binding"))]
pub bindings: Vec<Binding>,
}
#[derive(knus::Decode, Debug, Default)]
pub struct CacheNode {
#[knus(child, unwrap(argument))]
pub capacity: Option<String>,
#[knus(child, unwrap(argument))]
pub max_body: Option<String>,
#[knus(child, unwrap(argument))]
pub time_to_idle: Option<String>,
#[knus(child, unwrap(argument))]
pub time_to_live: Option<String>,
}
#[derive(knus::Decode, Debug, Clone)]
pub struct RateLimitNode {
#[knus(argument)]
pub rate: String,
#[knus(property)]
pub burst: Option<u64>,
}
#[derive(knus::Decode, Debug)]
pub struct Binding {
#[knus(argument)]
pub listen: String,
#[knus(child)]
pub tls: Option<TlsNode>,
#[knus(child)]
pub http: Option<HttpConfigNode>,
#[knus(children(name = "host"))]
pub hosts: Vec<HostBlock>,
#[knus(children(name = "route"))]
pub routes: Vec<Route>,
}
#[derive(knus::Decode, Debug)]
pub struct HostBlock {
#[knus(arguments)]
pub patterns: Vec<String>,
#[knus(child)]
pub tls: Option<TlsNode>,
#[knus(children(name = "route"))]
pub routes: Vec<Route>,
}
#[derive(knus::Decode, Debug)]
pub struct TlsNode {
#[knus(property)]
pub cert: PathBuf,
#[knus(property)]
pub key: PathBuf,
}
#[derive(knus::Decode, Debug, Default)]
pub struct HttpConfigNode {
#[knus(child, unwrap(argument))]
pub received_body_max_len: Option<String>,
#[knus(child, unwrap(argument))]
pub head_max_len: Option<String>,
#[knus(child, unwrap(argument))]
pub max_connections: Option<usize>,
}
#[derive(knus::Decode, Debug)]
pub struct Route {
#[knus(argument)]
pub pattern: String,
#[knus(children)]
pub directives: Vec<Directive>,
}
#[derive(knus::Decode, Debug)]
pub enum Directive {
Files(FilesDirective),
Proxy(ProxyDirective),
Redirect(RedirectDirective),
Headers(HeadersDirective),
RewriteHtml(RewriteHtmlDirective),
}
#[derive(knus::Decode, Debug)]
pub struct FilesDirective {
#[knus(property)]
pub root: PathBuf,
#[knus(property)]
pub index: Option<String>,
#[knus(property)]
pub directory_listing: Option<bool>,
}
#[derive(knus::Decode, Debug)]
pub struct ProxyDirective {
#[knus(property)]
pub strategy: Option<String>,
#[knus(children(name = "upstream"))]
pub upstreams: Vec<UpstreamNode>,
}
#[derive(knus::Decode, Debug)]
pub struct UpstreamNode {
#[knus(argument)]
pub url: String,
}
#[derive(knus::Decode, Debug)]
pub struct RedirectDirective {
#[knus(argument)]
pub to: String,
#[knus(property)]
pub status: Option<u16>,
}
#[derive(knus::Decode, Debug)]
pub struct HeadersDirective {
#[knus(children)]
pub ops: Vec<HeaderOp>,
}
#[derive(knus::Decode, Debug, Clone)]
pub enum HeaderOp {
Add(#[knus(argument)] String, #[knus(argument)] String),
Set(#[knus(argument)] String, #[knus(argument)] String),
Remove(#[knus(argument)] String),
}
#[derive(knus::Decode, Debug)]
pub struct RewriteHtmlDirective {
#[knus(children(name = "select"))]
pub selects: Vec<SelectBlock>,
}
#[derive(knus::Decode, Debug, Clone)]
pub struct SelectBlock {
#[knus(argument)]
pub selector: String,
#[knus(children)]
pub ops: Vec<ElementOp>,
}
#[derive(knus::Decode, Debug, Clone)]
pub enum ElementOp {
SetAttribute(#[knus(argument)] String, #[knus(argument)] String),
RemoveAttribute(#[knus(argument)] String),
Before(#[knus(argument)] String),
After(#[knus(argument)] String),
Prepend(#[knus(argument)] String),
Append(#[knus(argument)] String),
SetInner(#[knus(argument)] String),
SetText(#[knus(argument)] String),
Replace(#[knus(argument)] String),
SetTag(#[knus(argument)] String),
Remove,
Unwrap,
}
impl Config {
pub fn load(path: &std::path::Path) -> miette::Result<Self> {
let text = std::fs::read_to_string(path)
.map_err(|e| miette::miette!("could not read {}: {e}", path.display()))?;
let filename = path.display().to_string();
let config: Self = knus::parse(&filename, &text)?;
config.validate_selectors(&filename, &text)?;
Ok(config)
}
fn validate_selectors(&self, filename: &str, src: &str) -> miette::Result<()> {
use trillium_html_rewriter::html::Selector;
let routes = self
.bindings
.iter()
.flat_map(|b| b.hosts.iter().flat_map(|h| &h.routes).chain(&b.routes));
for route in routes {
for directive in &route.directives {
let Directive::RewriteHtml(rewrite) = directive else {
continue;
};
for block in &rewrite.selects {
if let Err(e) = block.selector.parse::<Selector>() {
let labels = locate(src, &block.selector)
.map(|span| vec![miette::LabeledSpan::at(span, "unsupported selector")])
.unwrap_or_default();
return Err(miette::miette!(
labels = labels,
help = "lol-html supports a subset of CSS selectors; see https://docs.rs/lol-html",
"invalid CSS selector {:?}: {e}",
block.selector,
)
.with_source_code(miette::NamedSource::new(filename, src.to_string())));
}
}
}
}
Ok(())
}
}
fn locate(src: &str, selector: &str) -> Option<miette::SourceSpan> {
let quoted = format!("\"{selector}\"");
src.find("ed)
.map(|start| miette::SourceSpan::from((start + 1, selector.len())))
}