spf: refactor EvalContext

This commit is contained in:
Dirkjan Ochtman
2024-10-12 08:00:15 -07:00
committed by Wez Furlong
parent 6da122eed0
commit 55cb583e61
3 changed files with 123 additions and 132 deletions
+115 -120
View File
@@ -1,139 +1,133 @@
use crate::record::{MacroElement, MacroName};
use std::collections::HashMap;
use std::fmt::Write;
use std::net::IpAddr;
use std::time::SystemTime;
#[derive(Default)]
pub struct EvalContext {
vars: HashMap<MacroName, String>,
pub struct EvalContext<'a> {
sender: &'a str,
local_part: &'a str,
sender_domain: &'a str,
domain: &'a str,
client_ip: IpAddr,
now: SystemTime,
}
impl EvalContext {
pub fn new() -> Self {
let mut ctx = Self::default();
impl<'a> EvalContext<'a> {
pub fn new(sender: &'a str, domain: &'a str, client_ip: IpAddr) -> Result<Self, String> {
let Some((local_part, sender_domain)) = sender.split_once('@') else {
return Err(format!(
"invalid sender {sender} is missing @ sign to delimit local part and domain"
));
};
ctx.set_var(
MacroName::CurrentUnixTimeStamp,
SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0),
);
ctx
}
pub fn set_sender<V: ToString>(&mut self, sender: V) -> Result<(), String> {
let sender = sender.to_string();
let (local, domain) = sender.split_once('@').ok_or_else(|| {
format!("invalid sender {sender} is missing @ sign to delimit local part and domain")
})?;
self.set_var(MacroName::LocalPart, local);
self.set_var(MacroName::SenderDomain, domain);
self.set_var(MacroName::Sender, sender);
Ok(())
}
pub fn set_ip<IP: Into<IpAddr>>(&mut self, ip: IP) {
let ip: IpAddr = ip.into();
self.set_var(MacroName::ClientIp, ip);
match ip {
IpAddr::V4(v4) => {
self.set_var(MacroName::Ip, v4);
}
IpAddr::V6(v6) => {
// For IPv6 addresses, the "i" macro expands to a dot-format address;
// it is intended for use in %{ir}.
let mut ip = String::new();
for segment in v6.segments() {
for b in format!("{segment:04x}").chars() {
if !ip.is_empty() {
ip.push('.');
}
ip.push(b);
}
}
self.set_var(MacroName::Ip, ip);
}
}
self.set_var(
MacroName::ReverseDns,
if ip.is_ipv4() { "in-addr" } else { "ip6" },
);
}
pub fn set_client_ip<IP: Into<IpAddr>>(&mut self, ip: IP) {
let ip: IpAddr = ip.into();
self.set_var(MacroName::ClientIp, ip);
}
pub fn set_var<V: ToString>(&mut self, name: MacroName, value: V) {
self.vars.insert(name, value.to_string());
Ok(Self {
sender,
local_part,
sender_domain,
domain,
client_ip,
now: SystemTime::now(),
})
}
pub fn evaluate(&self, elements: &[MacroElement]) -> Result<String, String> {
let mut result = String::new();
let (mut result, mut buf) = (String::new(), String::new());
for element in elements {
match element {
let m = match element {
MacroElement::Literal(t) => {
result.push_str(&t);
continue;
}
MacroElement::Macro(m) => {
eprintln!("apply {m:?}");
let value = self
.vars
.get(&m.name)
.ok_or_else(|| format!("{:?} has no been set in EvalContext", m.name))?;
let delimiters = if m.delimiters.is_empty() {
"."
} else {
&m.delimiters
};
let mut tokens: Vec<&str> = value.split(|c| delimiters.contains(c)).collect();
MacroElement::Macro(m) => m,
};
if m.reverse {
tokens.reverse();
buf.clear();
match m.name {
MacroName::Sender => buf.push_str(self.sender),
MacroName::LocalPart => buf.push_str(&self.local_part),
MacroName::SenderDomain => buf.push_str(&self.sender_domain),
MacroName::Domain => buf.push_str(&self.domain),
MacroName::ReverseDns => buf.push_str(match self.client_ip.is_ipv4() {
true => "in-addr",
false => "ip6",
}),
MacroName::ClientIp => {
buf.write_fmt(format_args!("{}", self.client_ip)).unwrap();
}
MacroName::Ip => match self.client_ip {
IpAddr::V4(v4) => {
buf.write_fmt(format_args!("{}", v4)).unwrap();
}
if let Some(n) = m.transformer_digits {
let n = n as usize;
while tokens.len() > n {
tokens.remove(0);
}
}
let output = tokens.join(".");
if m.url_escape {
// https://datatracker.ietf.org/doc/html/rfc7208#section-7.3:
// Uppercase macros expand exactly as their lowercase
// equivalents, and are then URL escaped. URL escaping
// MUST be performed for characters not in the
// "unreserved" set.
// https://datatracker.ietf.org/doc/html/rfc3986#section-2.3:
// unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
for c in output.chars() {
if c.is_ascii_alphanumeric()
|| c == '-'
|| c == '.'
|| c == '_'
|| c == '~'
{
result.push(c);
} else {
let mut bytes = [0u8; 4];
for b in c.encode_utf8(&mut bytes).bytes() {
result.push_str(&format!("%{b:02x}"));
IpAddr::V6(v6) => {
// For IPv6 addresses, the "i" macro expands to a dot-format address;
// it is intended for use in %{ir}.
for segment in v6.segments() {
for b in format!("{segment:04x}").chars() {
if !buf.is_empty() {
buf.push('.');
}
buf.push(b);
}
}
}
},
MacroName::CurrentUnixTimeStamp => buf
.write_fmt(format_args!(
"{}",
self.now
.duration_since(SystemTime::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
))
.unwrap(),
MacroName::RelayingHostName
| MacroName::HeloDomain
| MacroName::ValidatedDomainName => {
return Err(format!("{:?} has not been implemented", m.name))
}
};
let delimiters = if m.delimiters.is_empty() {
"."
} else {
&m.delimiters
};
let mut tokens: Vec<&str> = buf.split(|c| delimiters.contains(c)).collect();
if m.reverse {
tokens.reverse();
}
if let Some(n) = m.transformer_digits {
let n = n as usize;
while tokens.len() > n {
tokens.remove(0);
}
}
let output = tokens.join(".");
if m.url_escape {
// https://datatracker.ietf.org/doc/html/rfc7208#section-7.3:
// Uppercase macros expand exactly as their lowercase
// equivalents, and are then URL escaped. URL escaping
// MUST be performed for characters not in the
// "unreserved" set.
// https://datatracker.ietf.org/doc/html/rfc3986#section-2.3:
// unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
for c in output.chars() {
if c.is_ascii_alphanumeric() || c == '-' || c == '.' || c == '_' || c == '~' {
result.push(c);
} else {
result.push_str(&output);
let mut bytes = [0u8; 4];
for b in c.encode_utf8(&mut bytes).bytes() {
result.push_str(&format!("%{b:02x}"));
}
}
}
} else {
result.push_str(&output);
}
}
@@ -150,9 +144,12 @@ mod test {
fn test_eval() {
// <https://datatracker.ietf.org/doc/html/rfc7208#section-7.4>
let mut ctx = EvalContext::new();
ctx.set_sender("strong-bad@email.example.com").unwrap();
ctx.set_var(MacroName::Domain, "email.example.com");
let mut ctx = EvalContext::new(
"strong-bad@email.example.com",
"email.example.com",
IpAddr::from([192, 0, 2, 3]),
)
.unwrap();
for (input, expect) in &[
("%{s}", "strong-bad@email.example.com"),
@@ -175,8 +172,6 @@ mod test {
k9::assert_equal!(&output, expect, "{input}");
}
ctx.set_ip("192.0.2.3".parse::<IpAddr>().unwrap());
for (input, expect) in &[
(
"%{ir}.%{v}._spf.%{d2}",
@@ -202,7 +197,7 @@ mod test {
k9::assert_equal!(&output, expect, "{input}");
}
ctx.set_ip("2001:db8::cb01".parse::<IpAddr>().unwrap());
ctx.client_ip = IpAddr::from([0x2001, 0xdb8, 0, 0, 0, 0, 0, 0xcb01]);
for (input, expect) in &[
(
"%{ir}.%{v}._spf.%{d2}",
+6 -10
View File
@@ -6,7 +6,7 @@ use error::SpfError;
pub mod eval;
use eval::EvalContext;
pub mod record;
use record::{MacroName, Qualifier, Record};
use record::{Qualifier, Record};
#[cfg(test)]
mod tests;
@@ -105,19 +105,15 @@ impl CheckHostParams {
}
};
let mut cx = EvalContext::new();
cx.set_ip(self.client_ip);
if let Err(err) = cx.set_sender(&self.sender) {
return SpfResult {
disposition: SpfDisposition::TempError,
match EvalContext::new(&self.sender, &self.domain, self.client_ip) {
Ok(cx) => record.evaluate(&cx, resolver).await,
Err(err) => SpfResult {
disposition: SpfDisposition::PermError,
context: format!(
"input sender parameter '{}' is malformed: {err}",
self.sender
),
};
},
}
cx.set_var(MacroName::Domain, &self.domain);
record.evaluate(&cx, resolver).await
}
}
+2 -2
View File
@@ -33,7 +33,7 @@ impl Record {
Ok(Self { terms })
}
pub async fn evaluate(&self, cx: &EvalContext, resolver: &dyn Lookup) -> SpfResult {
pub async fn evaluate(&self, cx: &EvalContext<'_>, resolver: &dyn Lookup) -> SpfResult {
for term in &self.terms {
match term {
Term::Directive(d) => match d.evaluate(cx, resolver).await {
@@ -85,7 +85,7 @@ impl Directive {
pub async fn evaluate(
&self,
_cx: &EvalContext,
_cx: &EvalContext<'_>,
_resolver: &dyn Lookup,
) -> Result<Option<SpfResult>, SpfResult> {
let matched = match &self.mechanism {