Skip to main content

sofie_client/
settings.rs

1use std::time::Duration;
2
3use sofie_core::{Error, Result};
4
5use crate::target::{Target, normalized_host};
6
7const CONNECT_TIMEOUT: Duration = Duration::from_secs(5);
8const TOTAL_TIMEOUT: Duration = Duration::from_secs(30);
9const MAX_RESPONSE_BYTES: u64 = 4 * 1024 * 1024;
10const RETRIES: u32 = 2;
11
12/// Keys `<MODULE>_<VENDOR>_*`; all but `BASE_URL` have defaults, and only HTTP vendors need it.
13#[derive(Debug, Default)]
14pub struct Vendor {
15    pub base_url: Option<String>,
16    pub hosts: Option<String>,
17    /// Allows private and loopback addresses and plain HTTP; link-local and metadata stay refused.
18    pub private: Option<bool>,
19    pub connect_timeout_ms: Option<u64>,
20    pub timeout_ms: Option<u64>,
21    pub max_response_bytes: Option<u64>,
22    pub retries: Option<u32>,
23    pub redirects: Option<u32>,
24}
25
26pub(crate) struct Allowed {
27    /// Lowercase.
28    pub(crate) hosts: Vec<String>,
29    pub(crate) private: bool,
30}
31
32impl Allowed {
33    pub(crate) fn host(&self, host: &str) -> bool {
34        self.hosts.iter().any(|allowed| allowed == host)
35    }
36}
37
38#[derive(Clone, Copy, Debug)]
39pub(crate) struct Limits {
40    pub(crate) connect: Duration,
41    pub(crate) total: Duration,
42    pub(crate) response_bytes: usize,
43    pub(crate) retries: u32,
44    pub(crate) redirects: u32,
45}
46
47impl Vendor {
48    pub(crate) fn read(self, vendor: &str) -> Result<(Option<Target>, Allowed, Limits)> {
49        let base = self
50            .base_url
51            .as_deref()
52            .map(|url| {
53                let base = Target::parse(url)?;
54                match base.path.query() {
55                    Some(_) => {
56                        Err("it carries a query, which a path would be joined after".to_owned())
57                    }
58                    None => Ok(base),
59                }
60            })
61            .transpose()
62            .map_err(|problem| {
63                Error::internal_message(format!("{vendor}'s BASE_URL is refused: {problem}"))
64            })?;
65        let listed = self.hosts.as_deref().unwrap_or_default().split(',');
66        let mut hosts: Vec<String> = listed
67            .map(|host| normalized_host(host.trim()))
68            .filter(|host| !host.is_empty())
69            .collect();
70        hosts.extend(base.iter().map(|base| base.host.clone()));
71        let milliseconds =
72            |value: Option<u64>, default| value.map_or(default, Duration::from_millis);
73        let limits = Limits {
74            connect: milliseconds(self.connect_timeout_ms, CONNECT_TIMEOUT),
75            total: milliseconds(self.timeout_ms, TOTAL_TIMEOUT),
76            response_bytes: usize::try_from(self.max_response_bytes.unwrap_or(MAX_RESPONSE_BYTES))
77                .unwrap_or(usize::MAX),
78            retries: self.retries.unwrap_or(RETRIES),
79            redirects: self.redirects.unwrap_or(0),
80        };
81        let allowed = Allowed {
82            hosts,
83            private: self.private.unwrap_or(false),
84        };
85        Ok((base, allowed, limits))
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92
93    #[test]
94    fn a_base_url_with_a_query_is_refused() {
95        let settings = Vendor {
96            base_url: Some("https://api.vendor.test/v1?key=abc".to_owned()),
97            ..Vendor::default()
98        };
99
100        let Err(refused) = settings.read("vendor") else {
101            panic!("a base URL with a query is refused");
102        };
103
104        assert!(refused.to_string().contains("query"), "{refused}");
105    }
106
107    #[test]
108    fn a_listed_ipv6_host_matches_with_or_without_brackets() {
109        let settings = Vendor {
110            hosts: Some("[::1], 2001:DB8::1".to_owned()),
111            ..Vendor::default()
112        };
113
114        let (_, allowed, _) = settings.read("vendor").unwrap();
115
116        assert!(allowed.host("::1"));
117        assert!(allowed.host("2001:db8::1"));
118    }
119}