sn0int/autonoscope/
url.rs

1use crate::errors::*;
2use crate::autonoscope::{Autonoscope, ToRule, AutoRule, RulePrecision};
3use crate::models::*;
4use std::convert::TryFrom;
5
6#[derive(Debug, PartialEq)]
7pub struct UrlRule {
8    url: String,
9    origin: url::Origin,
10    segments: Vec<String>,
11}
12
13impl ToString for UrlRule {
14    fn to_string(&self) -> String {
15        self.url.clone()
16    }
17}
18
19impl TryFrom<Autonoscope> for UrlRule {
20    type Error = Error;
21
22    fn try_from(x: Autonoscope) -> Result<UrlRule> {
23        UrlRule::try_from(x.value.as_str())
24    }
25}
26
27impl TryFrom<&str> for UrlRule {
28    type Error = Error;
29
30    fn try_from(x: &str) -> Result<UrlRule> {
31        let url = x.parse::<url::Url>()?;
32
33        let origin = url.origin();
34
35        let segments = url.path_segments()
36            .ok_or_else(|| format_err!("url can't have a base"))?
37            .filter(|x| !x.is_empty())
38            .map(String::from)
39            .collect();
40
41        Ok(UrlRule {
42            url: x.to_string(),
43            origin,
44            segments,
45        })
46    }
47}
48
49// TODO: there is no way to write a rule that matches all urls
50impl AutoRule<Url> for UrlRule {
51    fn matches(&self, url: &Url) -> Result<bool> {
52        self.matches(url.value.as_str())
53    }
54}
55
56impl AutoRule<NewUrl> for UrlRule {
57    fn matches(&self, url: &NewUrl) -> Result<bool> {
58        self.matches(url.value.as_str())
59    }
60}
61
62impl AutoRule<str> for UrlRule {
63    fn matches(&self, url: &str) -> Result<bool> {
64        let url = url.parse::<url::Url>()?;
65
66        if url.origin() != self.origin {
67            return Ok(false);
68        }
69
70        let segments = url.path_segments()
71            .ok_or_else(|| format_err!("url can't have a base"))?
72            .filter(|x| !x.is_empty())
73            .collect::<Vec<_>>();
74
75        if self.segments.len() > segments.len() {
76            return Ok(false);
77        }
78
79        for (rule, path) in self.segments.iter().zip(segments.iter()) {
80            if rule != path {
81                return Ok(false);
82            }
83        }
84
85        Ok(true)
86    }
87}
88
89impl RulePrecision for UrlRule {
90    fn precision(&self) -> usize {
91        self.segments.len()
92    }
93}
94
95impl ToRule for UrlRule {
96    fn to_rule(&self) -> (&'static str, String) {
97        ("url", self.to_string())
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104    use std::convert::TryFrom;
105
106    #[test]
107    fn test_url_rule_explicit_origin() {
108        let rule = UrlRule::try_from("https://example.com").unwrap();
109        assert!(rule.matches("https://example.com:443/").unwrap());
110        assert_eq!(rule.precision(), 0);
111    }
112
113    #[test]
114    fn test_url_rule_scheme_mismatch() {
115        let rule = UrlRule::try_from("https://example.com").unwrap();
116        assert!(!rule.matches("http://example.com:443/").unwrap());
117        assert_eq!(rule.precision(), 0);
118    }
119
120    #[test]
121    fn test_url_rule_port_mismatch() {
122        let rule = UrlRule::try_from("https://example.com").unwrap();
123        assert!(!rule.matches("https://example.com:80/").unwrap());
124        assert_eq!(rule.precision(), 0);
125    }
126
127    #[test]
128    fn test_url_rule_subdomain_mismatch1() {
129        let rule = UrlRule::try_from("https://example.com").unwrap();
130        assert!(!rule.matches("https://www.example.com/").unwrap());
131        assert_eq!(rule.precision(), 0);
132    }
133
134    #[test]
135    fn test_url_rule_subdomain_mismatch2() {
136        let rule = UrlRule::try_from("https://www.example.com").unwrap();
137        assert!(!rule.matches("https://example.com/").unwrap());
138        assert_eq!(rule.precision(), 0);
139    }
140
141    #[test]
142    fn test_url_rule_ftp() {
143        let rule = UrlRule::try_from("ftp://www.example.com").unwrap();
144        assert!(!rule.matches("https://example.com/").unwrap());
145        assert_eq!(rule.precision(), 0);
146    }
147
148    #[test]
149    fn test_url_rule_outside_of_path() {
150        let rule = UrlRule::try_from("https://www.example.com/asset").unwrap();
151        assert!(!rule.matches("https://example.com/").unwrap());
152        assert_eq!(rule.precision(), 1);
153    }
154
155    #[test]
156    fn test_url_rule_path_match_implicit_slash() {
157        let rule = UrlRule::try_from("https://www.example.com/asset").unwrap();
158        assert!(rule.matches("https://www.example.com/asset/").unwrap());
159        assert_eq!(rule.precision(), 1);
160    }
161
162    #[test]
163    fn test_url_rule_path_match_explicit_slash() {
164        let rule = UrlRule::try_from("https://www.example.com/asset/").unwrap();
165        assert!(rule.matches("https://www.example.com/asset").unwrap());
166        assert_eq!(rule.precision(), 1);
167    }
168
169    #[test]
170    fn test_url_rule_in_folder_implicit_slash() {
171        let rule = UrlRule::try_from("https://www.example.com/asset").unwrap();
172        assert!(rule.matches("https://www.example.com/asset/style.css").unwrap());
173        assert_eq!(rule.precision(), 1);
174    }
175
176    #[test]
177    fn test_url_rule_in_folder_explicit_slash() {
178        let rule = UrlRule::try_from("https://www.example.com/asset/").unwrap();
179        assert!(rule.matches("https://www.example.com/asset/style.css").unwrap());
180        assert_eq!(rule.precision(), 1);
181    }
182}