torrust_tracker/console/clients/checker/
config.rs

1use std::error::Error;
2use std::fmt;
3
4use reqwest::Url as ServiceUrl;
5use serde::Deserialize;
6
7/// It parses the configuration from a JSON format.
8///
9/// # Errors
10///
11/// Will return an error if the configuration is not valid.
12///
13/// # Panics
14///
15/// Will panic if unable to read the configuration file.
16pub fn parse_from_json(json: &str) -> Result<Configuration, ConfigurationError> {
17    let plain_config: PlainConfiguration = serde_json::from_str(json).map_err(ConfigurationError::JsonParseError)?;
18    Configuration::try_from(plain_config)
19}
20
21/// DTO for the configuration to serialize/deserialize configuration.
22///
23/// Configuration does not need to be valid.
24#[derive(Deserialize)]
25struct PlainConfiguration {
26    pub udp_trackers: Vec<String>,
27    pub http_trackers: Vec<String>,
28    pub health_checks: Vec<String>,
29}
30
31/// Validated configuration
32pub struct Configuration {
33    pub udp_trackers: Vec<ServiceUrl>,
34    pub http_trackers: Vec<ServiceUrl>,
35    pub health_checks: Vec<ServiceUrl>,
36}
37
38#[derive(Debug)]
39pub enum ConfigurationError {
40    JsonParseError(serde_json::Error),
41    InvalidUdpAddress(std::net::AddrParseError),
42    InvalidUrl(url::ParseError),
43}
44
45impl Error for ConfigurationError {}
46
47impl fmt::Display for ConfigurationError {
48    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49        match self {
50            ConfigurationError::JsonParseError(e) => write!(f, "JSON parse error: {e}"),
51            ConfigurationError::InvalidUdpAddress(e) => write!(f, "Invalid UDP address: {e}"),
52            ConfigurationError::InvalidUrl(e) => write!(f, "Invalid URL: {e}"),
53        }
54    }
55}
56
57impl TryFrom<PlainConfiguration> for Configuration {
58    type Error = ConfigurationError;
59
60    fn try_from(plain_config: PlainConfiguration) -> Result<Self, Self::Error> {
61        let udp_trackers = plain_config
62            .udp_trackers
63            .into_iter()
64            .map(|s| if s.starts_with("udp://") { s } else { format!("udp://{s}") })
65            .map(|s| s.parse::<ServiceUrl>().map_err(ConfigurationError::InvalidUrl))
66            .collect::<Result<Vec<_>, _>>()?;
67
68        let http_trackers = plain_config
69            .http_trackers
70            .into_iter()
71            .map(|s| s.parse::<ServiceUrl>().map_err(ConfigurationError::InvalidUrl))
72            .collect::<Result<Vec<_>, _>>()?;
73
74        let health_checks = plain_config
75            .health_checks
76            .into_iter()
77            .map(|s| s.parse::<ServiceUrl>().map_err(ConfigurationError::InvalidUrl))
78            .collect::<Result<Vec<_>, _>>()?;
79
80        Ok(Configuration {
81            udp_trackers,
82            http_trackers,
83            health_checks,
84        })
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91
92    #[test]
93    fn configuration_should_be_build_from_plain_serializable_configuration() {
94        let dto = PlainConfiguration {
95            udp_trackers: vec!["udp://127.0.0.1:8080".to_string()],
96            http_trackers: vec!["http://127.0.0.1:8080".to_string()],
97            health_checks: vec!["http://127.0.0.1:8080/health".to_string()],
98        };
99
100        let config = Configuration::try_from(dto).expect("A valid configuration");
101
102        assert_eq!(config.udp_trackers, vec![ServiceUrl::parse("udp://127.0.0.1:8080").unwrap()]);
103
104        assert_eq!(
105            config.http_trackers,
106            vec![ServiceUrl::parse("http://127.0.0.1:8080").unwrap()]
107        );
108
109        assert_eq!(
110            config.health_checks,
111            vec![ServiceUrl::parse("http://127.0.0.1:8080/health").unwrap()]
112        );
113    }
114
115    mod building_configuration_from_plain_configuration_for {
116
117        mod udp_trackers {
118            use crate::console::clients::checker::config::{Configuration, PlainConfiguration, ServiceUrl};
119
120            /* The plain configuration should allow UDP URLs with:
121
122            - IP or domain.
123            - With or without scheme.
124            - With or without `announce` suffix.
125            - With or without `/` at the end of the authority section (with empty path).
126
127            For example:
128
129            127.0.0.1:6969
130            127.0.0.1:6969/
131            127.0.0.1:6969/announce
132
133            localhost:6969
134            localhost:6969/
135            localhost:6969/announce
136
137            udp://127.0.0.1:6969
138            udp://127.0.0.1:6969/
139            udp://127.0.0.1:6969/announce
140
141            udp://localhost:6969
142            udp://localhost:6969/
143            udp://localhost:6969/announce
144
145            */
146
147            #[test]
148            fn it_should_fail_when_a_tracker_udp_url_is_invalid() {
149                let plain_config = PlainConfiguration {
150                    udp_trackers: vec!["invalid URL".to_string()],
151                    http_trackers: vec![],
152                    health_checks: vec![],
153                };
154
155                assert!(Configuration::try_from(plain_config).is_err());
156            }
157
158            #[test]
159            fn it_should_add_the_udp_scheme_to_the_udp_url_when_it_is_missing() {
160                let plain_config = PlainConfiguration {
161                    udp_trackers: vec!["127.0.0.1:6969".to_string()],
162                    http_trackers: vec![],
163                    health_checks: vec![],
164                };
165
166                let config = Configuration::try_from(plain_config).expect("Invalid plain configuration");
167
168                assert_eq!(config.udp_trackers[0], "udp://127.0.0.1:6969".parse::<ServiceUrl>().unwrap());
169            }
170
171            #[test]
172            fn it_should_allow_using_domains() {
173                let plain_config = PlainConfiguration {
174                    udp_trackers: vec!["udp://localhost:6969".to_string()],
175                    http_trackers: vec![],
176                    health_checks: vec![],
177                };
178
179                let config = Configuration::try_from(plain_config).expect("Invalid plain configuration");
180
181                assert_eq!(config.udp_trackers[0], "udp://localhost:6969".parse::<ServiceUrl>().unwrap());
182            }
183
184            #[test]
185            fn it_should_allow_the_url_to_have_an_empty_path() {
186                let plain_config = PlainConfiguration {
187                    udp_trackers: vec!["127.0.0.1:6969/".to_string()],
188                    http_trackers: vec![],
189                    health_checks: vec![],
190                };
191
192                let config = Configuration::try_from(plain_config).expect("Invalid plain configuration");
193
194                assert_eq!(config.udp_trackers[0], "udp://127.0.0.1:6969/".parse::<ServiceUrl>().unwrap());
195            }
196
197            #[test]
198            fn it_should_allow_the_url_to_contain_a_path() {
199                // This is the common format for UDP tracker URLs:
200                // udp://domain.com:6969/announce
201
202                let plain_config = PlainConfiguration {
203                    udp_trackers: vec!["127.0.0.1:6969/announce".to_string()],
204                    http_trackers: vec![],
205                    health_checks: vec![],
206                };
207
208                let config = Configuration::try_from(plain_config).expect("Invalid plain configuration");
209
210                assert_eq!(
211                    config.udp_trackers[0],
212                    "udp://127.0.0.1:6969/announce".parse::<ServiceUrl>().unwrap()
213                );
214            }
215        }
216
217        mod http_trackers {
218            use crate::console::clients::checker::config::{Configuration, PlainConfiguration, ServiceUrl};
219
220            #[test]
221            fn it_should_fail_when_a_tracker_http_url_is_invalid() {
222                let plain_config = PlainConfiguration {
223                    udp_trackers: vec![],
224                    http_trackers: vec!["invalid URL".to_string()],
225                    health_checks: vec![],
226                };
227
228                assert!(Configuration::try_from(plain_config).is_err());
229            }
230
231            #[test]
232            fn it_should_allow_the_url_to_contain_a_path() {
233                // This is the common format for HTTP tracker URLs:
234                // http://domain.com:7070/announce
235
236                let plain_config = PlainConfiguration {
237                    udp_trackers: vec![],
238                    http_trackers: vec!["http://127.0.0.1:7070/announce".to_string()],
239                    health_checks: vec![],
240                };
241
242                let config = Configuration::try_from(plain_config).expect("Invalid plain configuration");
243
244                assert_eq!(
245                    config.http_trackers[0],
246                    "http://127.0.0.1:7070/announce".parse::<ServiceUrl>().unwrap()
247                );
248            }
249
250            #[test]
251            fn it_should_allow_the_url_to_contain_an_empty_path() {
252                let plain_config = PlainConfiguration {
253                    udp_trackers: vec![],
254                    http_trackers: vec!["http://127.0.0.1:7070/".to_string()],
255                    health_checks: vec![],
256                };
257
258                let config = Configuration::try_from(plain_config).expect("Invalid plain configuration");
259
260                assert_eq!(
261                    config.http_trackers[0],
262                    "http://127.0.0.1:7070/".parse::<ServiceUrl>().unwrap()
263                );
264            }
265        }
266
267        mod health_checks {
268            use crate::console::clients::checker::config::{Configuration, PlainConfiguration};
269
270            #[test]
271            fn it_should_fail_when_a_health_check_http_url_is_invalid() {
272                let plain_config = PlainConfiguration {
273                    udp_trackers: vec![],
274                    http_trackers: vec![],
275                    health_checks: vec!["invalid URL".to_string()],
276                };
277
278                assert!(Configuration::try_from(plain_config).is_err());
279            }
280        }
281    }
282}