viaduct/
settings.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5use once_cell::sync::Lazy;
6use parking_lot::RwLock;
7use std::time::Duration;
8use url::Url;
9
10/// Note: reqwest allows these only to be specified per-Client. concept-fetch
11/// allows these to be specified on each call to fetch. I think it's worth
12/// keeping a single global reqwest::Client in the reqwest backend, to simplify
13/// the way we abstract away from these.
14///
15/// In the future, should we need it, we might be able to add a CustomClient type
16/// with custom settings. In the reqwest backend this would store a Client, and
17/// in the concept-fetch backend it would only store the settings, and populate
18/// things on the fly.
19#[derive(Debug)]
20#[non_exhaustive]
21pub struct Settings {
22    pub read_timeout: Option<Duration>,
23    pub connect_timeout: Option<Duration>,
24    pub follow_redirects: bool,
25    pub use_caches: bool,
26    // For testing purposes, we allow exactly one additional Url which is
27    // allowed to not be https.
28    //
29    // Note: this is the only setting the new backend code uses.  Once all applications have moved
30    // away from the legacy backend, we can delete all other fields.
31    pub addn_allowed_insecure_url: Option<Url>,
32}
33
34#[cfg(target_os = "ios")]
35const TIMEOUT_DURATION: Duration = Duration::from_secs(7);
36
37#[cfg(not(target_os = "ios"))]
38const TIMEOUT_DURATION: Duration = Duration::from_secs(10);
39
40// The singleton instance of our settings.
41pub static GLOBAL_SETTINGS: Lazy<RwLock<Settings>> = Lazy::new(|| {
42    RwLock::new(Settings {
43        read_timeout: Some(TIMEOUT_DURATION),
44        connect_timeout: Some(TIMEOUT_DURATION),
45        follow_redirects: true,
46        use_caches: false,
47        addn_allowed_insecure_url: None,
48    })
49});
50
51/// Allow non-HTTPS requests to the emulator loopback URL
52#[uniffi::export]
53pub fn allow_android_emulator_loopback() {
54    let url = url::Url::parse("http://10.0.2.2").unwrap();
55    let mut settings = GLOBAL_SETTINGS.write();
56    settings.addn_allowed_insecure_url = Some(url);
57}
58
59/// Validate a request, respecting the `addn_allowed_insecure_url` setting.
60pub fn validate_request(request: &crate::Request) -> Result<(), crate::ViaductError> {
61    if request.url.scheme() != "https"
62        && match request.url.host() {
63            Some(url::Host::Domain(d)) => d != "localhost",
64            Some(url::Host::Ipv4(addr)) => !addr.is_loopback(),
65            Some(url::Host::Ipv6(addr)) => !addr.is_loopback(),
66            None => true,
67        }
68        && {
69            let settings = GLOBAL_SETTINGS.read();
70            settings
71                .addn_allowed_insecure_url
72                .as_ref()
73                .map(|url| url.host() != request.url.host() || url.scheme() != request.url.scheme())
74                .unwrap_or(true)
75        }
76    {
77        return Err(crate::ViaductError::NonTlsUrl);
78    }
79    Ok(())
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85
86    #[test]
87    fn test_validate_request() {
88        let _https_request = crate::Request::new(
89            crate::Method::Get,
90            url::Url::parse("https://www.example.com").unwrap(),
91        );
92        assert!(validate_request(&_https_request).is_ok());
93
94        let _http_request = crate::Request::new(
95            crate::Method::Get,
96            url::Url::parse("http://www.example.com").unwrap(),
97        );
98        assert!(validate_request(&_http_request).is_err());
99
100        let _localhost_https_request = crate::Request::new(
101            crate::Method::Get,
102            url::Url::parse("https://127.0.0.1/index.html").unwrap(),
103        );
104        assert!(validate_request(&_localhost_https_request).is_ok());
105
106        let _localhost_https_request_2 = crate::Request::new(
107            crate::Method::Get,
108            url::Url::parse("https://localhost:4242/").unwrap(),
109        );
110        assert!(validate_request(&_localhost_https_request_2).is_ok());
111
112        let _localhost_http_request = crate::Request::new(
113            crate::Method::Get,
114            url::Url::parse("http://localhost:4242/").unwrap(),
115        );
116        assert!(validate_request(&_localhost_http_request).is_ok());
117
118        let localhost_request = crate::Request::new(
119            crate::Method::Get,
120            url::Url::parse("localhost:4242/").unwrap(),
121        );
122        assert!(validate_request(&localhost_request).is_err());
123
124        let localhost_request_shorthand_ipv6 =
125            crate::Request::new(crate::Method::Get, url::Url::parse("http://[::1]").unwrap());
126        assert!(validate_request(&localhost_request_shorthand_ipv6).is_ok());
127
128        let localhost_request_ipv6 = crate::Request::new(
129            crate::Method::Get,
130            url::Url::parse("http://[0:0:0:0:0:0:0:1]").unwrap(),
131        );
132        assert!(validate_request(&localhost_request_ipv6).is_ok());
133    }
134
135    #[test]
136    fn test_validate_request_addn_allowed_insecure_url() {
137        let request_root = crate::Request::new(
138            crate::Method::Get,
139            url::Url::parse("http://anything").unwrap(),
140        );
141        let request = crate::Request::new(
142            crate::Method::Get,
143            url::Url::parse("http://anything/path").unwrap(),
144        );
145        // This should never be accepted.
146        let request_ftp = crate::Request::new(
147            crate::Method::Get,
148            url::Url::parse("ftp://anything/path").unwrap(),
149        );
150        assert!(validate_request(&request_root).is_err());
151        assert!(validate_request(&request).is_err());
152        {
153            let mut settings = GLOBAL_SETTINGS.write();
154            settings.addn_allowed_insecure_url =
155                Some(url::Url::parse("http://something-else").unwrap());
156        }
157        assert!(validate_request(&request_root).is_err());
158        assert!(validate_request(&request).is_err());
159
160        {
161            let mut settings = GLOBAL_SETTINGS.write();
162            settings.addn_allowed_insecure_url = Some(url::Url::parse("http://anything").unwrap());
163        }
164        assert!(validate_request(&request_root).is_ok());
165        assert!(validate_request(&request).is_ok());
166        assert!(validate_request(&request_ftp).is_err());
167    }
168}