Skip to main content

prosa_utils/
config.rs

1//! Module for ProSA configuration object
2//!
3//! <svg width="40" height="40">
4#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/doc_assets/settings.svg"))]
5//! </svg>
6
7use std::{io, path::PathBuf, process::Command};
8
9use base64::{Engine as _, engine::general_purpose::STANDARD};
10use thiserror::Error;
11use url::Url;
12use uuid::Uuid;
13
14// Feature openssl or rusttls,...
15pub mod ssl;
16
17// Feature opentelemetry
18#[cfg(feature = "config-observability")]
19pub mod observability;
20
21// Feature tracing
22#[cfg(feature = "config-observability")]
23pub mod tracing;
24
25/// Error define for configuration object
26#[derive(Debug, Error)]
27pub enum ConfigError {
28    /// Error that indicate a wrong path format in filesystem
29    #[error("The config parameter {0} have an incorrect value `{1}`")]
30    WrongValue(String, String),
31    /// Error that indicate a wrong path format pattern in filesystem
32    #[error("The path `{0}` provided don't match the pattern `{1}`")]
33    WrongPathPattern(String, glob::PatternError),
34    /// Error that indicate a wrong path format in filesystem
35    #[error("The path `{0}` provided is not correct")]
36    WrongPath(PathBuf),
37    /// Error on a file read
38    #[error("The file `{0}` can't be read `{1}`")]
39    IoFile(String, std::io::Error),
40    #[cfg(feature = "config-openssl")]
41    /// SSL error
42    #[error("Openssl error `{0}`")]
43    OpenSsl(#[from] openssl::error::ErrorStack),
44}
45
46impl From<ConfigError> for io::Error {
47    fn from(err: ConfigError) -> Self {
48        io::Error::new(
49            io::ErrorKind::InvalidInput,
50            format!("ProSA Config error: {}", err),
51        )
52    }
53}
54
55/// Method to try get the country name from the OS
56pub fn os_country() -> Option<String> {
57    if let Some(lang) = option_env!("LANG") {
58        let language = if let Some(pos) = lang.find('.') {
59            &lang[..pos]
60        } else {
61            lang
62        };
63
64        if let Some(pos) = language.find('_') {
65            return Some(String::from(&language[pos + 1..]));
66        }
67    }
68
69    None
70}
71
72/// Method to try get the hostname from the OS
73pub fn hostname() -> Option<String> {
74    #[cfg(target_family = "unix")]
75    if let Ok(host) = std::env::var("HOSTNAME").map(|h| h.trim().to_string())
76        && !host.is_empty()
77        && !host.contains('\n')
78    {
79        return Some(host);
80    }
81
82    #[cfg(target_family = "unix")]
83    return Command::new("hostname")
84        .arg("-s")
85        .output()
86        .ok()
87        .and_then(|h| {
88            str::from_utf8(h.stdout.trim_ascii())
89                .ok()
90                .filter(|h| !h.is_empty() && !h.contains('\n'))
91                .map(|h| h.to_string())
92        });
93
94    #[cfg(target_family = "windows")]
95    return Command::new("hostname").output().ok().and_then(|h| {
96        str::from_utf8(h.stdout.trim_ascii())
97            .ok()
98            .filter(|h| !h.is_empty() && !h.contains('\n'))
99            .map(|h| h.to_string())
100    });
101
102    #[cfg(all(not(target_family = "unix"), not(target_family = "windows")))]
103    return None;
104}
105
106/// Method to get a consistant host ID (UUID v1 or UUID v4) useful for `service.instance.id`
107pub fn hostid() -> String {
108    #[cfg(target_os = "linux")]
109    if let Ok(machine_id) = std::fs::read_to_string("/etc/machine-id")
110        && let Ok(machine_uuid) = Uuid::parse_str(machine_id.trim())
111    {
112        return machine_uuid.to_string();
113    }
114
115    #[cfg(target_os = "macos")]
116    if let Ok(output) = Command::new("ioreg")
117        .args(["-rd1", "-c", "IOPlatformExpertDevice"])
118        .output()
119        && output.status.success()
120        && let Ok(output_str) = String::from_utf8(output.stdout)
121    {
122        for line in output_str.lines() {
123            if line.contains("IOPlatformUUID")
124                && let Some(value) = line.split('"').nth(3)
125                && let Ok(machine_uuid) = Uuid::parse_str(value.trim())
126            {
127                return machine_uuid.to_string();
128            }
129        }
130    }
131
132    if let Some(hostname) = hostname() {
133        let mut node_id = [0u8; 6];
134        let len = hostname.len().min(6);
135        node_id[..len].copy_from_slice(&hostname.as_bytes()[hostname.len() - len..]);
136
137        Uuid::now_v1(&node_id).to_string()
138    } else {
139        Uuid::new_v4().to_string()
140    }
141}
142
143/// Method to get authentication value out of URL username/password
144///
145/// - If user password is provided, it return *Basic* authentication with base64 encoded username:password
146/// - If only password is provided, it return *Bearer* authentication with the password as token
147///
148/// ```
149/// use url::Url;
150/// use prosa_utils::config::url_authentication;
151///
152/// let basic_auth_target = Url::parse("http://user:pass@localhost:8080").unwrap();
153/// assert_eq!(Some(String::from("Basic dXNlcjpwYXNz")), url_authentication(&basic_auth_target));
154///
155/// let bearer_auth_target = Url::parse("http://:token@localhost:8080").unwrap();
156/// assert_eq!(Some(String::from("Bearer token")), url_authentication(&bearer_auth_target));
157/// ```
158pub fn url_authentication(url: &Url) -> Option<String> {
159    if let Some(password) = url.password().map(|p| {
160        p.replace("%24", "$")
161            .replace("%26", "&")
162            .replace("%3D", "=")
163    }) {
164        if url.username().is_empty() {
165            Some(format!("Bearer {password}"))
166        } else {
167            Some(format!(
168                "Basic {}",
169                STANDARD.encode(format!("{}:{}", url.username(), password))
170            ))
171        }
172    } else {
173        None
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180
181    #[test]
182    fn test_os_country() {
183        let country = os_country();
184        if let Some(cn) = country {
185            assert_eq!(2, cn.len());
186        }
187    }
188
189    #[test]
190    fn test_hostname() {
191        let host = hostname();
192        if let Some(hn) = host {
193            assert!(!hn.is_empty());
194        }
195    }
196
197    #[test]
198    fn test_hostid() {
199        let host_id = hostid();
200        assert_eq!(host_id.len(), 36);
201    }
202
203    #[test]
204    fn test_url_authentication_basic() {
205        let basic_auth_target = Url::parse("http://user:pass@localhost:8080")
206            .expect("Basic auth target URL should be valid");
207        assert_eq!(
208            Some(String::from("Basic dXNlcjpwYXNz")),
209            url_authentication(&basic_auth_target)
210        );
211    }
212
213    #[test]
214    fn test_url_safe_authentication_basic() {
215        let basic_auth_target = Url::parse("http://user:$ab&cd=@localhost:8080")
216            .expect("Basic auth target URL should be valid");
217        assert_eq!(
218            Some(String::from("Basic dXNlcjokYWImY2Q9")),
219            url_authentication(&basic_auth_target)
220        );
221    }
222
223    #[test]
224    fn test_url_authentication_bearer() {
225        let bearer_auth_target = Url::parse("http://:token@localhost:8080")
226            .expect("Basic auth target URL should be valid");
227        assert_eq!(
228            Some(String::from("Bearer token")),
229            url_authentication(&bearer_auth_target)
230        );
231    }
232}