mod_rewrite/conditions/
mod.rs

1use std::str::FromStr;
2
3pub mod context;
4mod error;
5mod matcher;
6mod parse;
7
8use matcher::{Match, Value};
9
10pub use context::EngineCtx;
11pub use error::CondError;
12
13/// Singular `RewriteCond` expression definition.
14///
15/// It contains an expression matcher and additional flags that
16/// define how the rule behaves within the rule-engine.
17///
18/// Supports a subset of [offical](https://httpd.apache.org/docs/current/mod/mod_rewrite.html#rewritecond)
19/// mod_rewrite rules.
20#[derive(Clone, Debug)]
21pub struct Condition {
22    matcher: Match,
23    flags: Vec<CondFlag>,
24}
25
26impl Condition {
27    /// Evaluate if the rewrite condition and return boolean result.
28    pub fn is_met(&self, ctx: &mut EngineCtx) -> bool {
29        let nocase = self.flags.iter().any(|f| matches!(f, CondFlag::NoCase));
30        match &self.matcher {
31            Match::Pattern(v1, pt, v2) => {
32                pt.matches(Value::new(v1, nocase, ctx), Value::new(v2, nocase, ctx))
33            }
34            Match::NotPattern(v1, pt, v2) => {
35                !pt.matches(Value::new(v1, nocase, ctx), Value::new(v2, nocase, ctx))
36            }
37            Match::Compare(v1, cp, v2) => {
38                cp.compare(Value::new(v1, nocase, ctx), Value::new(v2, nocase, ctx))
39            }
40            Match::FileTest(v1, ft) => ft.matches(Value::new(v1, nocase, ctx)),
41            Match::NotFileTest(v1, ft) => !ft.matches(Value::new(v1, nocase, ctx)),
42        }
43    }
44
45    /// Returns true if the rewrite condition uses OR operator rather
46    /// than the default AND.
47    #[inline]
48    pub fn is_or(&self) -> bool {
49        self.flags.iter().any(|c| matches!(c, CondFlag::Or))
50    }
51}
52
53impl FromStr for Condition {
54    type Err = CondError;
55
56    fn from_str(s: &str) -> Result<Self, Self::Err> {
57        let mut tokens = parse::tokenize(s)?.into_iter().peekable();
58        let matcher = Match::parse(&mut tokens)?;
59        let flags = match tokens.next() {
60            Some(flags) => CondFlagList::from_str(&flags)?.0,
61            None => Vec::new(),
62        };
63        Ok(Self { matcher, flags })
64    }
65}
66
67struct CondFlagList(Vec<CondFlag>);
68
69impl FromStr for CondFlagList {
70    type Err = CondError;
71
72    fn from_str(s: &str) -> Result<Self, Self::Err> {
73        if !s.starts_with('[') || !s.ends_with(']') {
74            return Err(CondError::FlagsMissingBrackets(s.to_owned()));
75        }
76        let flags = s[1..s.len() - 1]
77            .split(',')
78            .map(|s| s.trim())
79            .filter(|s| !s.is_empty())
80            .map(CondFlag::from_str)
81            .collect::<Result<Vec<CondFlag>, _>>()?;
82        if flags.is_empty() {
83            return Err(CondError::FlagsEmpty);
84        }
85        Ok(Self(flags))
86    }
87}
88
89/// Supported `mod_rewrite` [`Condition`] flags that modify
90/// the conditions behavior.
91#[derive(Clone, Debug)]
92enum CondFlag {
93    NoCase,
94    Or,
95}
96
97impl FromStr for CondFlag {
98    type Err = CondError;
99
100    fn from_str(s: &str) -> Result<Self, Self::Err> {
101        match s.to_lowercase().as_str() {
102            "i" | "insensitive" | "nc" | "nocase" => Ok(Self::NoCase),
103            "or" | "ornext" => Ok(Self::Or),
104            _ => Err(CondError::InvalidFlag(s.to_owned())),
105        }
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    use context::{RequestCtx, ServerCtx};
114    use matcher::{Compare, FileTest, Pattern};
115
116    #[test]
117    fn test_pattern() {
118        let s1 = String::from("%{REQUEST_URI}");
119        let s2 = String::from("/Test");
120        let cond = Condition::from_str(&format!(r#"{s1} "={s2}" [NC,OR]"#)).unwrap();
121        assert!(matches!(
122            &cond.matcher,
123            Match::Pattern(v1, Pattern::Equals, v2) if v1 == &s1 && v2 == &s2,
124        ));
125        assert_eq!(cond.flags.len(), 2);
126        assert!(matches!(cond.flags.get(0), Some(CondFlag::NoCase)));
127
128        let mut req = RequestCtx::default().request_uri("/Test");
129        let mut ctx = EngineCtx::default().with_ctx(req);
130        assert!(cond.is_met(&mut ctx));
131
132        req = RequestCtx::default().request_uri("/Not");
133        let mut ctx = EngineCtx::default().with_ctx(req);
134        assert!(!cond.is_met(&mut ctx));
135    }
136
137    #[test]
138    fn test_compare() {
139        let s1 = String::from("%{SERVER_PORT}");
140        let s2 = String::from("4000");
141        let cond = Condition::from_str(&format!("{s1} -ge {s2}")).unwrap();
142        assert!(matches!(
143            &cond.matcher,
144            Match::Compare(v1, Compare::GreaterOrEqual, v2) if v1 == &s1 && v2 == &s2,
145        ));
146        assert_eq!(cond.flags.len(), 0);
147
148        let mut srv = ServerCtx::default().server_addr("127.0.0.1:4001").unwrap();
149        let mut ctx = EngineCtx::default().with_ctx(srv);
150        assert!(cond.is_met(&mut ctx));
151
152        srv = ServerCtx::default().server_addr("127.0.0.1:3999").unwrap();
153        let mut ctx = EngineCtx::default().with_ctx(srv);
154        assert!(!cond.is_met(&mut ctx));
155    }
156
157    #[test]
158    fn test_filetest() {
159        let s1 = String::from("%{REQUEST_URI}");
160        let cond = Condition::from_str(&format!("{s1} !-f")).unwrap();
161        assert!(matches!(
162            &cond.matcher,
163            Match::NotFileTest(v1, FileTest::File) if v1 == &s1,
164        ));
165        assert_eq!(cond.flags.len(), 0);
166
167        let current = std::env::current_dir().unwrap().join("src").join("lib.rs");
168        let mut req = RequestCtx::default().request_uri(current.to_str().unwrap());
169        let mut ctx = EngineCtx::default().with_ctx(req);
170        assert!(!cond.is_met(&mut ctx));
171
172        req = RequestCtx::default().request_uri("/invalid");
173        let mut ctx = EngineCtx::default().with_ctx(req);
174        assert!(cond.is_met(&mut ctx));
175    }
176}