mod port_forward;
mod redirect_https;
mod reverse_proxy;
mod static_site;
use serde_json::Value;
use crate::error::Error;
use crate::rule::{ListenSpec, RawRule, SourceInfo, TlsConfig};
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
pub struct PresetInvocation {
pub name: String,
pub preset: String,
#[serde(deserialize_with = "crate::rule::de_listen_non_empty")]
pub listen: Vec<ListenSpec>,
#[serde(default)]
pub args: Value,
#[serde(default)]
pub tls: Option<TlsConfig>,
#[serde(default)]
pub source: SourceInfo,
}
#[derive(Debug, Clone, serde::Serialize)]
#[serde(untagged)]
pub enum RuleEntry {
Preset(PresetInvocation),
Raw(RawRule),
}
impl<'de> serde::Deserialize<'de> for RuleEntry {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
let v = Value::deserialize(d)?;
if v.get("preset").is_some() {
let inv: PresetInvocation = serde_json::from_value(v).map_err(serde::de::Error::custom)?;
Ok(Self::Preset(inv))
} else {
let r: RawRule = serde_json::from_value(v).map_err(serde::de::Error::custom)?;
Ok(Self::Raw(r))
}
}
}
pub fn expand_invocation(inv: PresetInvocation) -> Result<Vec<RawRule>, Error> {
match inv.preset.as_str() {
"reverse_proxy" => reverse_proxy::expand(inv),
"port_forward" => port_forward::expand(inv),
"static_site" => static_site::expand(inv),
"redirect_https" => redirect_https::expand(inv),
other => Err(Error::compile(format!(
"unknown preset {other:?}; supported: reverse_proxy / port_forward / static_site / redirect_https"
))),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unknown_preset_name_yields_compile_error() {
let inv = PresetInvocation {
name: "x".into(),
preset: "no_such_preset".into(),
listen: vec![":443".into()],
args: Value::Null,
tls: None,
source: SourceInfo::default(),
};
let err = expand_invocation(inv).expect_err("unknown preset must fail");
let msg = err.to_string();
assert!(msg.contains("no_such_preset"), "error names the offending preset: {msg}");
assert!(msg.contains("reverse_proxy"), "error lists supported presets: {msg}");
}
#[test]
fn rule_entry_deserializes_preset_when_preset_key_present() {
let raw = serde_json::json!({
"preset": "port_forward",
"name": "ssh",
"listen": [":2222"],
"args": { "upstream": "10.0.0.5:22" }
});
let entry: RuleEntry = serde_json::from_value(raw).expect("parse preset entry");
match entry {
RuleEntry::Preset(inv) => {
assert_eq!(inv.preset, "port_forward");
assert_eq!(inv.name, "ssh");
assert_eq!(inv.listen, vec![":2222".to_string()]);
}
RuleEntry::Raw(_) => panic!("preset key must route to Preset variant"),
}
}
#[test]
fn rule_entry_deserializes_raw_when_no_preset_key() {
let raw = serde_json::json!({
"name": "r",
"listen": [":443"],
"terminate": { "type": "http_proxy", "upstream": "127.0.0.1:8080" }
});
let entry: RuleEntry = serde_json::from_value(raw).expect("parse raw entry");
match entry {
RuleEntry::Raw(r) => assert_eq!(r.name, "r"),
RuleEntry::Preset(_) => panic!("no preset key must route to Raw variant"),
}
}
}