1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245
// viaspf – implementation of the SPF specification
// Copyright © 2020–2023 David Bürgin <dbuergin@gluet.ch>
//
// This program is free software: you can redistribute it and/or modify it under
// the terms of the GNU General Public License as published by the Free Software
// Foundation, either version 3 of the License, or (at your option) any later
// version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
// details.
//
// You should have received a copy of the GNU General Public License along with
// this program. If not, see https://www.gnu.org/licenses/.
use crate::record::ExplainString;
use std::{
fmt::{self, Debug, Formatter},
time::Duration,
};
/// A builder for SPF query configurations.
pub struct ConfigBuilder {
max_lookups: usize,
max_void_lookups: usize,
timeout: Duration,
modify_exp_fn: Option<Box<dyn Fn(&mut ExplainString) + Send + Sync>>,
hostname: Option<String>,
capture_trace: bool,
}
impl ConfigBuilder {
/// The maximum number of DNS lookups allowed. The default is 10.
///
/// This setting is used separately for both the global, per-query limit,
/// and the per-mechanism limit for the *mx* and *ptr* mechanisms.
///
/// This setting **must not** be changed from the default 10, if conformance
/// with RFC 7208 is desired. It is provided for exceptional circumstances
/// only.
pub fn max_lookups(mut self, value: usize) -> Self {
self.max_lookups = value;
self
}
/// The maximum number of void lookups allowed. The default is 2.
pub fn max_void_lookups(mut self, value: usize) -> Self {
self.max_void_lookups = value;
self
}
/// The duration until a query times out. The default is 20 seconds.
///
/// A lookup implementation may contain its own timeout logic, to make sure
/// each request doesn’t take too much time. This setting is independent
/// from the lookup implementation, a query will simply be dropped when this
/// timeout expires.
///
/// Note: The timeout is only effective when the (default) feature
/// `tokio-timeout` is enabled.
pub fn timeout(mut self, value: Duration) -> Self {
self.timeout = value;
self
}
/// The function to apply to explain strings originating from the DNS via an
/// *exp* modifier.
///
/// This function serves as a callback that allows sanitising or otherwise
/// altering the third-party-provided explain string before it undergoes
/// macro expansion. An example use would be prepending a macro string such
/// as `"%{o} explains: "`.
pub fn modify_exp_with<F>(mut self, f: F) -> Self
where
F: Fn(&mut ExplainString) + Send + Sync + 'static,
{
self.modify_exp_fn = Some(Box::new(f));
self
}
/// The hostname of the SPF verifier.
///
/// This is substituted for the `r` macro during macro expansion. The given
/// string is used as-is, without validation or transformation.
pub fn hostname<S>(mut self, value: S) -> Self
where
S: Into<String>,
{
self.hostname = Some(value.into());
self
}
/// Whether to capture a trace during query execution.
///
/// Note: Enabling tracing has a cost, as the data structures encountered
/// during processing are copied and aggregated in the trace.
pub fn capture_trace(mut self, value: bool) -> Self {
self.capture_trace = value;
self
}
/// Constructs a new configuration.
pub fn build(self) -> Config {
Config {
max_lookups: self.max_lookups,
max_void_lookups: self.max_void_lookups,
timeout: self.timeout,
modify_exp_fn: self.modify_exp_fn,
hostname: self.hostname,
capture_trace: self.capture_trace,
}
}
}
impl Default for ConfigBuilder {
fn default() -> Self {
Self {
// §4.6.4: ‘The following terms cause DNS queries […]. SPF
// implementations MUST limit the total number of those terms to 10
// during SPF evaluation’. Two distinct lookup limits – global and
// per-mechanism – both use this value and are not separately
// configurable for now.
max_lookups: 10,
// ‘SPF implementations SHOULD limit "void lookups" to two. An
// implementation MAY choose to make such a limit configurable. In
// this case, a default of two is RECOMMENDED.’
max_void_lookups: 2,
// ‘MTAs or other processors SHOULD impose a limit on the maximum
// amount of elapsed time to evaluate check_host(). Such a limit
// SHOULD allow at least 20 seconds.’
timeout: Duration::from_secs(20),
modify_exp_fn: None,
hostname: None,
capture_trace: false,
}
}
}
/// An SPF query configuration.
pub struct Config {
// Note: Keep fields in sync with the `Debug` implementation below.
max_lookups: usize,
max_void_lookups: usize,
timeout: Duration,
modify_exp_fn: Option<Box<dyn Fn(&mut ExplainString) + Send + Sync>>,
hostname: Option<String>,
capture_trace: bool,
}
impl Config {
/// Creates a builder for configurations.
pub fn builder() -> ConfigBuilder {
Default::default()
}
/// The maximum number of DNS lookups allowed.
pub fn max_lookups(&self) -> usize {
self.max_lookups
}
/// The maximum number of void lookups allowed.
pub fn max_void_lookups(&self) -> usize {
self.max_void_lookups
}
/// The duration until a query times out.
pub fn timeout(&self) -> Duration {
self.timeout
}
/// The function to apply to explain strings originating from the DNS via an
/// *exp* modifier.
pub fn modify_exp_fn(&self) -> Option<&(impl Fn(&mut ExplainString) + Send + Sync)> {
self.modify_exp_fn.as_ref()
}
/// The hostname of the SPF verifier.
pub fn hostname(&self) -> Option<&str> {
self.hostname.as_deref()
}
/// Whether to capture a trace during query execution.
pub fn capture_trace(&self) -> bool {
self.capture_trace
}
}
impl Default for Config {
fn default() -> Self {
ConfigBuilder::default().build()
}
}
// A manual `Debug` implementation is needed to accomodate the `modify_exp_fn`
// closure, but should otherwise be exactly like a derived one.
impl Debug for Config {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_struct("Config")
.field("max_lookups", &self.max_lookups)
.field("max_void_lookups", &self.max_void_lookups)
.field("timeout", &self.timeout)
.field("modify_exp_fn", &"<closure>")
.field("hostname", &self.hostname)
.field("capture_trace", &self.capture_trace)
.finish()
}
}
impl From<ConfigBuilder> for Config {
fn from(builder: ConfigBuilder) -> Self {
builder.build()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_void_lookups_ok() {
assert_eq!(Config::default().max_void_lookups(), 2);
}
#[test]
fn modify_exp_with_ok() {
let prefix = "%{o} explains: ".to_owned();
let config = Config::builder()
.modify_exp_with(move |explain_string| {
let prefix = prefix.parse::<ExplainString>().unwrap();
explain_string.segments.splice(..0, prefix);
})
.build();
let f = config.modify_exp_fn().unwrap();
let mut explain_string = "not authorized".parse().unwrap();
f(&mut explain_string);
assert_eq!(
explain_string,
"%{o} explains: not authorized".parse().unwrap()
);
}
}