use openmoat_core::{Decision, METHOD_OVERRIDE_HEADERS, RequestPath, SecretRoute, Verdict};
pub const RULE_REQUEST_RULE: &str = "proxy-request-rule";
#[derive(Debug, Default)]
pub struct Requests {
hosts: Vec<HostRules>,
}
#[derive(Debug)]
struct HostRules {
host: String,
deny: Vec<(String, SecretRoute)>,
}
#[derive(Debug, thiserror::Error)]
#[error("request rules for {host}: {why}")]
pub struct RequestRuleError {
host: String,
why: String,
}
impl Requests {
pub fn deny(mut self, host: &str, deny: &[&str]) -> Result<Self, RequestRuleError> {
let fail = |why: String| RequestRuleError {
host: host.to_owned(),
why,
};
if host.is_empty() || host.contains(['*', ':', '/']) {
return Err(fail("the host must be one DNS name".to_owned()));
}
let mut rules = Vec::with_capacity(deny.len());
for text in deny {
let folded = text.split_once(' ').map_or_else(
|| (*text).to_owned(),
|(m, p)| format!("{m} {}", p.to_ascii_lowercase()),
);
rules.push((
(*text).to_owned(),
SecretRoute::parse(&folded).map_err(fail)?,
));
}
self.hosts.push(HostRules {
host: host.to_ascii_lowercase(),
deny: rules,
});
Ok(self)
}
#[must_use = "the hosts are only listed"]
pub fn hosts(&self) -> impl Iterator<Item = &str> {
self.hosts.iter().map(|h| h.host.as_str())
}
pub(crate) fn names(&self, host: &str) -> bool {
self.rules(host).is_some()
}
fn rules(&self, host: &str) -> Option<&HostRules> {
self.hosts
.iter()
.find(|h| h.host.eq_ignore_ascii_case(host))
}
pub(crate) fn check(&self, host: &str, head: &[u8]) -> Result<Option<String>, Decision> {
let Some(rules) = self.rules(host) else {
return Ok(None);
};
let refuse = |why: String| Decision::single(Verdict::Deny, RULE_REQUEST_RULE, why);
let mut headers = [httparse::EMPTY_HEADER; crate::request::MAX_HEADERS + 2];
let mut request = httparse::Request::new(&mut headers);
let (Ok(httparse::Status::Complete(_)), Some(method), Some(target)) =
(request.parse(head), request.method, request.path)
else {
return Err(refuse(format!(
"the request to {host} could not be read again to check its request rules"
)));
};
let method = method.to_ascii_uppercase();
let path = RequestPath::parse(&target.to_ascii_lowercase()).map_err(|why| {
refuse(format!(
"{method} request to {host}: {why}, so the request rules cannot tell what the \
server would read"
))
})?;
let overriding = request.headers.iter().find(|h| {
METHOD_OVERRIDE_HEADERS
.iter()
.any(|o| h.name.eq_ignore_ascii_case(o))
});
if let Some(header) = overriding {
return Err(refuse(format!(
"{method} request to {host}: its `{}` header asks the server for another method",
header.name
)));
}
if path.overrides_method() {
return Err(refuse(format!(
"{method} request to {host}: its query names a `_method` parameter"
)));
}
match rules
.deny
.iter()
.find(|(_, rule)| rule.matches(&method, &path))
{
Some((text, _)) => Err(refuse(format!(
"{method} request to {host} is refused by the request rule `deny: {text}` \
(ADR-062 proof of concept)"
))),
None => Ok(Some(format!(
"no request rule for {host} refuses this {method} request"
))),
}
}
}
#[cfg(test)]
mod tests;