1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

use crate::GLOBAL_SETTINGS;
use ffi::FfiBackend;
use once_cell::sync::OnceCell;
mod ffi;

pub fn note_backend(which: &str) {
    // If trace logs are enabled: log on every request. Otherwise, just log on
    // the first request at `info` level. We remember if the Once was triggered
    // to avoid logging twice in the first case.
    static NOTE_BACKEND_ONCE: std::sync::Once = std::sync::Once::new();
    let mut called = false;
    NOTE_BACKEND_ONCE.call_once(|| {
        log::info!("Using HTTP backend {}", which);
        called = true;
    });
    if !called {
        log::trace!("Using HTTP backend {}", which);
    }
}

pub trait Backend: Send + Sync + 'static {
    fn send(&self, request: crate::Request) -> Result<crate::Response, crate::Error>;
}

static BACKEND: OnceCell<&'static dyn Backend> = OnceCell::new();

pub fn set_backend(b: &'static dyn Backend) -> Result<(), crate::Error> {
    BACKEND
        .set(b)
        .map_err(|_| crate::error::Error::SetBackendError)
}

pub(crate) fn get_backend() -> &'static dyn Backend {
    *BACKEND.get_or_init(|| Box::leak(Box::new(FfiBackend)))
}

pub fn send(request: crate::Request) -> Result<crate::Response, crate::Error> {
    validate_request(&request)?;
    get_backend().send(request)
}

pub fn validate_request(request: &crate::Request) -> Result<(), crate::Error> {
    if request.url.scheme() != "https"
        && match request.url.host() {
            Some(url::Host::Domain(d)) => d != "localhost",
            Some(url::Host::Ipv4(addr)) => !addr.is_loopback(),
            Some(url::Host::Ipv6(addr)) => !addr.is_loopback(),
            None => true,
        }
        && {
            let settings = GLOBAL_SETTINGS.read();
            settings
                .addn_allowed_insecure_url
                .as_ref()
                .map(|url| url.host() != request.url.host() || url.scheme() != request.url.scheme())
                .unwrap_or(true)
        }
    {
        return Err(crate::Error::NonTlsUrl);
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_validate_request() {
        let _https_request = crate::Request::new(
            crate::Method::Get,
            url::Url::parse("https://www.example.com").unwrap(),
        );
        assert!(validate_request(&_https_request).is_ok());

        let _http_request = crate::Request::new(
            crate::Method::Get,
            url::Url::parse("http://www.example.com").unwrap(),
        );
        assert!(validate_request(&_http_request).is_err());

        let _localhost_https_request = crate::Request::new(
            crate::Method::Get,
            url::Url::parse("https://127.0.0.1/index.html").unwrap(),
        );
        assert!(validate_request(&_localhost_https_request).is_ok());

        let _localhost_https_request_2 = crate::Request::new(
            crate::Method::Get,
            url::Url::parse("https://localhost:4242/").unwrap(),
        );
        assert!(validate_request(&_localhost_https_request_2).is_ok());

        let _localhost_http_request = crate::Request::new(
            crate::Method::Get,
            url::Url::parse("http://localhost:4242/").unwrap(),
        );
        assert!(validate_request(&_localhost_http_request).is_ok());

        let localhost_request = crate::Request::new(
            crate::Method::Get,
            url::Url::parse("localhost:4242/").unwrap(),
        );
        assert!(validate_request(&localhost_request).is_err());

        let localhost_request_shorthand_ipv6 =
            crate::Request::new(crate::Method::Get, url::Url::parse("http://[::1]").unwrap());
        assert!(validate_request(&localhost_request_shorthand_ipv6).is_ok());

        let localhost_request_ipv6 = crate::Request::new(
            crate::Method::Get,
            url::Url::parse("http://[0:0:0:0:0:0:0:1]").unwrap(),
        );
        assert!(validate_request(&localhost_request_ipv6).is_ok());
    }

    #[test]
    fn test_validate_request_addn_allowed_insecure_url() {
        let request_root = crate::Request::new(
            crate::Method::Get,
            url::Url::parse("http://anything").unwrap(),
        );
        let request = crate::Request::new(
            crate::Method::Get,
            url::Url::parse("http://anything/path").unwrap(),
        );
        // This should never be accepted.
        let request_ftp = crate::Request::new(
            crate::Method::Get,
            url::Url::parse("ftp://anything/path").unwrap(),
        );
        assert!(validate_request(&request_root).is_err());
        assert!(validate_request(&request).is_err());
        {
            let mut settings = GLOBAL_SETTINGS.write();
            settings.addn_allowed_insecure_url =
                Some(url::Url::parse("http://something-else").unwrap());
        }
        assert!(validate_request(&request_root).is_err());
        assert!(validate_request(&request).is_err());

        {
            let mut settings = GLOBAL_SETTINGS.write();
            settings.addn_allowed_insecure_url = Some(url::Url::parse("http://anything").unwrap());
        }
        assert!(validate_request(&request_root).is_ok());
        assert!(validate_request(&request).is_ok());
        assert!(validate_request(&request_ftp).is_err());
    }
}