https_everywhere_lib_core/
rewriter.rs

1use url::Url;
2use std::error::Error;
3use regex::Regex;
4
5use crate::{RuleSet, RuleSets, Storage};
6#[cfg(all(test,feature="add_rulesets"))]
7use crate::rulesets::tests as rulesets_tests;
8
9/// A RewriteAction is used to indicate an action to take, returned by the rewrite_url method on
10/// the Rewriter struct
11#[derive(Debug)]
12#[derive(PartialEq)]
13pub enum RewriteAction {
14    CancelRequest,
15    NoOp,
16    RewriteUrl(String),
17}
18
19/// A Rewriter provides an abstraction layer over RuleSets and Storage, providing the logic for
20/// rewriting URLs
21pub struct Rewriter<'a> {
22    rulesets: &'a RuleSets,
23    storage: &'a dyn Storage,
24}
25
26impl<'a> Rewriter<'a> {
27    /// Returns a rewriter with the rulesets and storage engine specified
28    ///
29    /// # Arguments
30    ///
31    /// * `rulesets` - An instance of RuleSets for rewriting URLs
32    /// * `storage` - A storage object to query current state
33    pub fn new(rulesets: &'a RuleSets, storage: &'a dyn Storage) -> Rewriter<'a> {
34        Rewriter {
35            rulesets,
36            storage,
37        }
38    }
39
40    /// Return a RewriteAction wrapped in a Result when given a URL.  This action should be
41    /// ingested by the implementation using the library
42    ///
43    /// # Arguments
44    ///
45    /// * `url` - A URL to determine the action for
46    pub fn rewrite_url(&self, url: &String) -> Result<RewriteAction, Box<dyn Error>> {
47        if let Some(false) = self.storage.get_bool(String::from("global_enabled")){
48            return Ok(RewriteAction::NoOp);
49        }
50
51        let mut url = Url::parse(url)?;
52        if let Some(hostname) = url.host_str() {
53            let mut hostname = hostname.trim_end_matches('.');
54            if hostname.len() == 0 {
55                hostname = ".";
56            }
57            let hostname = hostname.to_string();
58
59            let mut should_cancel = false;
60            let http_nowhere_on = self.storage.get_bool(String::from("http_nowhere_on"));
61            if let Some(true) = http_nowhere_on {
62                if url.scheme() == "http" || url.scheme() == "ftp" {
63                    let num_localhost = Regex::new(r"^127(\.[0-9]{1,3}){3}$").unwrap();
64                    if !hostname.ends_with(".onion") &&
65                        hostname != "localhost".to_string() &&
66                        !num_localhost.is_match(&hostname) &&
67                        hostname != "0.0.0.0".to_string() &&
68                        hostname != "[::1]".to_string() {
69                        should_cancel = true;
70                    }
71                }
72            }
73            let mut using_credentials_in_url = false;
74            let tmp_url = url.clone();
75            if url.username() != "" || url.password() != None {
76                using_credentials_in_url = true;
77                url.set_username("").unwrap();
78                url.set_password(None).unwrap();
79            }
80
81            let mut new_url: Option<Url> = None;
82
83            let mut apply_if_active = |ruleset: &RuleSet| {
84                if ruleset.active && new_url.is_none() {
85                    new_url = match ruleset.apply(url.as_str()) {
86                        None => None,
87                        Some(url_str) => Some(Url::parse(&url_str).unwrap())
88                    };
89                }
90            };
91
92
93            for ruleset in self.rulesets.potentially_applicable(&hostname) {
94                if let Some(scope) = (*ruleset.scope).clone() {
95                    let scope_regex = Regex::new(&scope).unwrap();
96                    if scope_regex.is_match(url.as_str()) {
97                        apply_if_active(&ruleset);
98                    }
99                } else {
100                    apply_if_active(&ruleset);
101                }
102            }
103
104            if using_credentials_in_url {
105                match &mut new_url {
106                    None => {
107                        url.set_username(tmp_url.username()).unwrap();
108                        url.set_password(tmp_url.password()).unwrap();
109                    },
110                    Some(url) => {
111                        url.set_username(tmp_url.username()).unwrap();
112                        url.set_password(tmp_url.password()).unwrap();
113                    }
114                }
115            }
116
117            if let Some(true) = http_nowhere_on {
118                if should_cancel {
119                    if new_url.is_none() {
120                        return Ok(RewriteAction::CancelRequest);
121                    }
122                }
123
124                // Cancel if we're about to redirect to HTTP or FTP in EASE mode
125                if let Some(url) = &new_url {
126                    if url.as_str().starts_with("http:") ||
127                       url.as_str().starts_with("ftp:") {
128                        return Ok(RewriteAction::CancelRequest);
129                    }
130                }
131            }
132
133            if let Some(url) = new_url {
134                info!("rewrite_url returning redirect url: {}", url.as_str());
135                Ok(RewriteAction::RewriteUrl(url.as_str().to_string()))
136            } else {
137                Ok(RewriteAction::NoOp)
138            }
139        } else {
140            Ok(RewriteAction::NoOp)
141        }
142    }
143}
144
145#[cfg(all(test,feature="add_rulesets"))]
146mod tests {
147    use super::*;
148    use multi_default_trait_impl::{default_trait_impl, trait_impl};
149
150    #[default_trait_impl]
151    impl Storage for DefaultStorage {
152        fn get_int(&self, _key: String) -> Option<usize> { Some(5) }
153        fn set_int(&self, _key: String, _value: usize) {}
154        fn get_string(&self, _key: String) -> Option<String> { Some(String::from("test")) }
155        fn set_string(&self, _key: String, _value: String) {}
156        fn get_bool(&self, key: String) -> Option<bool> {
157            if key == String::from("http_nowhere_on") {
158                Some(false)
159            } else {
160                Some(true)
161            }
162        }
163        fn set_bool(&self, _key: String, _value: bool) {}
164    }
165
166    struct TestStorage;
167    #[trait_impl]
168    impl DefaultStorage for TestStorage {
169    }
170
171    struct HttpNowhereOnStorage;
172    #[trait_impl]
173    impl DefaultStorage for HttpNowhereOnStorage {
174        fn get_bool(&self, _key: String) -> Option<bool> { Some(true) }
175    }
176
177    #[test]
178    fn rewrite_url() {
179        let mut rs = RuleSets::new();
180        rulesets_tests::add_mock_rulesets(&mut rs);
181
182        let rw = Rewriter::new(&rs, &TestStorage);
183
184        assert_eq!(
185            rw.rewrite_url(&String::from("http://freerangekitten.com/")).unwrap(),
186            RewriteAction::RewriteUrl(String::from("https://freerangekitten.com/")));
187
188        assert_eq!(
189            rw.rewrite_url(&String::from("http://fake-example.com/")).unwrap(),
190            RewriteAction::NoOp);
191    }
192
193    #[test]
194    fn rewrite_url_http_nowhere_on() {
195        let mut rs = RuleSets::new();
196        rulesets_tests::add_mock_rulesets(&mut rs);
197
198        let rw = Rewriter::new(&rs, &HttpNowhereOnStorage);
199
200        assert_eq!(
201            rw.rewrite_url(&String::from("http://freerangekitten.com/")).unwrap(),
202            RewriteAction::RewriteUrl(String::from("https://freerangekitten.com/")));
203
204        assert_eq!(
205            rw.rewrite_url(&String::from("http://fake-example.com/")).unwrap(),
206            RewriteAction::CancelRequest);
207
208        assert_eq!(
209            rw.rewrite_url(&String::from("http://fake-example.onion/")).unwrap(),
210            RewriteAction::NoOp);
211
212        assert_eq!(
213            rw.rewrite_url(&String::from("http://fake-example.onion..../")).unwrap(),
214            RewriteAction::NoOp);
215    }
216
217    #[test]
218    fn rewrite_exclusions() {
219        let mut rs = RuleSets::new();
220        rulesets_tests::add_mock_rulesets(&mut rs);
221
222        let rw = Rewriter::new(&rs, &TestStorage);
223
224        assert_eq!(
225            rw.rewrite_url(&String::from("http://chart.googleapis.com/")).unwrap(),
226            RewriteAction::NoOp);
227
228        assert_eq!(
229            rw.rewrite_url(&String::from("http://chart.googleapis.com/123")).unwrap(),
230            RewriteAction::RewriteUrl(String::from("https://chart.googleapis.com/123")));
231    }
232
233    #[test]
234    fn rewrite_with_credentials() {
235        let mut rs = RuleSets::new();
236        rulesets_tests::add_mock_rulesets(&mut rs);
237
238        let rw = Rewriter::new(&rs, &TestStorage);
239
240        assert_eq!(
241            rw.rewrite_url(&String::from("http://eff:techprojects@chart.googleapis.com/123")).unwrap(),
242            RewriteAction::RewriteUrl(String::from("https://eff:techprojects@chart.googleapis.com/123")));
243    }
244}