openmoat_proxy/
requests.rs1use openmoat_core::{Decision, METHOD_OVERRIDE_HEADERS, RequestPath, SecretRoute, Verdict};
18
19pub const RULE_REQUEST_RULE: &str = "proxy-request-rule";
22
23#[derive(Debug, Default)]
25pub struct Requests {
26 hosts: Vec<HostRules>,
27}
28
29#[derive(Debug)]
31struct HostRules {
32 host: String,
33 deny: Vec<(String, SecretRoute)>,
34}
35
36#[derive(Debug, thiserror::Error)]
38#[error("request rules for {host}: {why}")]
39pub struct RequestRuleError {
40 host: String,
41 why: String,
42}
43
44impl Requests {
45 pub fn deny(mut self, host: &str, deny: &[&str]) -> Result<Self, RequestRuleError> {
48 let fail = |why: String| RequestRuleError {
49 host: host.to_owned(),
50 why,
51 };
52 if host.is_empty() || host.contains(['*', ':', '/']) {
53 return Err(fail("the host must be one DNS name".to_owned()));
54 }
55 let mut rules = Vec::with_capacity(deny.len());
56 for text in deny {
57 let folded = text.split_once(' ').map_or_else(
59 || (*text).to_owned(),
60 |(m, p)| format!("{m} {}", p.to_ascii_lowercase()),
61 );
62 rules.push((
63 (*text).to_owned(),
64 SecretRoute::parse(&folded).map_err(fail)?,
65 ));
66 }
67 self.hosts.push(HostRules {
68 host: host.to_ascii_lowercase(),
69 deny: rules,
70 });
71 Ok(self)
72 }
73
74 #[must_use = "the hosts are only listed"]
76 pub fn hosts(&self) -> impl Iterator<Item = &str> {
77 self.hosts.iter().map(|h| h.host.as_str())
78 }
79
80 pub(crate) fn names(&self, host: &str) -> bool {
82 self.rules(host).is_some()
83 }
84
85 fn rules(&self, host: &str) -> Option<&HostRules> {
86 self.hosts
87 .iter()
88 .find(|h| h.host.eq_ignore_ascii_case(host))
89 }
90
91 pub(crate) fn check(&self, host: &str, head: &[u8]) -> Result<Option<String>, Decision> {
96 let Some(rules) = self.rules(host) else {
97 return Ok(None);
98 };
99 let refuse = |why: String| Decision::single(Verdict::Deny, RULE_REQUEST_RULE, why);
100 let mut headers = [httparse::EMPTY_HEADER; crate::request::MAX_HEADERS + 2];
101 let mut request = httparse::Request::new(&mut headers);
102 let (Ok(httparse::Status::Complete(_)), Some(method), Some(target)) =
103 (request.parse(head), request.method, request.path)
104 else {
105 return Err(refuse(format!(
106 "the request to {host} could not be read again to check its request rules"
107 )));
108 };
109 let method = method.to_ascii_uppercase();
110 let path = RequestPath::parse(&target.to_ascii_lowercase()).map_err(|why| {
111 refuse(format!(
112 "{method} request to {host}: {why}, so the request rules cannot tell what the \
113 server would read"
114 ))
115 })?;
116 let overriding = request.headers.iter().find(|h| {
117 METHOD_OVERRIDE_HEADERS
118 .iter()
119 .any(|o| h.name.eq_ignore_ascii_case(o))
120 });
121 if let Some(header) = overriding {
122 return Err(refuse(format!(
123 "{method} request to {host}: its `{}` header asks the server for another method",
124 header.name
125 )));
126 }
127 if path.overrides_method() {
128 return Err(refuse(format!(
129 "{method} request to {host}: its query names a `_method` parameter"
130 )));
131 }
132 match rules
133 .deny
134 .iter()
135 .find(|(_, rule)| rule.matches(&method, &path))
136 {
137 Some((text, _)) => Err(refuse(format!(
138 "{method} request to {host} is refused by the request rule `deny: {text}` \
139 (ADR-062 proof of concept)"
140 ))),
141 None => Ok(Some(format!(
142 "no request rule for {host} refuses this {method} request"
143 ))),
144 }
145 }
146}
147
148#[cfg(test)]
149mod tests;