1#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/doc_assets/settings.svg"))]
5use 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
14pub mod ssl;
16
17#[cfg(feature = "config-observability")]
19pub mod observability;
20
21#[cfg(feature = "config-observability")]
23pub mod tracing;
24
25#[derive(Debug, Error)]
27pub enum ConfigError {
28 #[error("The config parameter {0} have an incorrect value `{1}`")]
30 WrongValue(String, String),
31 #[error("The path `{0}` provided don't match the pattern `{1}`")]
33 WrongPathPattern(String, glob::PatternError),
34 #[error("The path `{0}` provided is not correct")]
36 WrongPath(PathBuf),
37 #[error("The file `{0}` can't be read `{1}`")]
39 IoFile(String, std::io::Error),
40 #[cfg(feature = "config-openssl")]
41 #[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
55pub 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
72pub 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
106pub 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
143pub 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}