Skip to main content

openmoat_proxy/
requests.rs

1//! Method and path rules for a host with no brokered secret (ADR-062, step 0:
2//! the proof of concept). A host listed here is terminated in `openmoat run`
3//! like the host of a `tls: terminate` secret (ADR-043), and each request read
4//! inside its tunnel is refused when a `deny` rule names it, before anything
5//! goes upstream; every other request passes unchanged.
6//!
7//! The rules are ADR-043's `METHOD /path` rules (`openmoat_core::SecretRoute`),
8//! matched the same way, with two differences that make a deny list err wide:
9//! the method and the path are compared in any letter case (a server may read
10//! `post` or `/Gists` as the route the rule names), and a request whose path
11//! the server could read as another, or that asks the server for another
12//! method, is refused instead of being matched.
13//!
14//! There is no policy schema for these rules yet: the CLI builds them from an
15//! unlisted setting of `openmoat run`, which ADR-062 step 0 measures.
16
17use openmoat_core::{Decision, METHOD_OVERRIDE_HEADERS, RequestPath, SecretRoute, Verdict};
18
19/// Rule id of a request a host's request rules refuse. ADR-062 proposes
20/// `proxy-request`, which already names a malformed request (`RULE_REQUEST`).
21pub const RULE_REQUEST_RULE: &str = "proxy-request-rule";
22
23/// The request rules of one proxy run. [`Requests::default`] holds none.
24#[derive(Debug, Default)]
25pub struct Requests {
26    hosts: Vec<HostRules>,
27}
28
29/// One host's `deny` rules, each with the text it was written as.
30#[derive(Debug)]
31struct HostRules {
32    host: String,
33    deny: Vec<(String, SecretRoute)>,
34}
35
36/// A request rule that cannot be used.
37#[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    /// These rules, and `host` refused every request one of `deny` names
46    /// (`METHOD /path`, as a secret's `allow` rules are written).
47    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            // The method stays as written: `Route::parse` wants it in capitals.
58            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    /// The hosts these rules name, as the CA must cover them.
75    #[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    /// Whether `host` has rules, so its tunnels are terminated.
81    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    /// Check `head` (the complete request head the proxy will send) to
92    /// `host`. `Ok` holds the reason for the audit row when `host` has rules;
93    /// `Err` the deny to record. The reasons name the method and the rule,
94    /// never the path, which may carry data.
95    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;