xapi_rs/
config.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2
3use crate::{Mode, MyLanguageTag};
4use base64::{prelude::BASE64_STANDARD, Engine};
5use chrono::TimeDelta;
6use dotenvy::var;
7use std::{
8    num::NonZeroUsize,
9    path::{self, Path, PathBuf},
10    str::FromStr,
11    sync::OnceLock,
12    time::Duration,
13};
14use tracing::{info, warn};
15
16// NOTE (rsn) 20241204 - if these values change make sure the documentation
17// in `.env.template` matches...
18const DEFAULT_TTL_BATCH_LEN: &str = "50";
19const DEFAULT_TTL_SECS: &str = "30";
20const DEFAULT_TTL_INTERVAL_SECS: &str = "60";
21
22const DEFAULT_MFC_INTERVAL_SECS: &str = "10";
23
24const DEPRECATION_MSG1: &str =
25    "LRS_AUTHORITY_IFI is now deprecated and will be removed in future release.\nUse LRS_ROOT_EMAIL instead.";
26
27static CONFIG: OnceLock<Config> = OnceLock::new();
28/// This LRS server configuration Singleton.
29pub fn config() -> &'static Config {
30    CONFIG.get_or_init(Config::default)
31}
32
33/// A structure that provides the current configuration settings.
34#[derive(Debug)]
35pub struct Config {
36    pub(crate) db_server_url: String,
37    pub(crate) db_name: String,
38    pub(crate) db_max_connections: u32,
39    pub(crate) db_min_connections: u32,
40    pub(crate) db_acquire_timeout: Duration,
41    pub(crate) db_idle_timeout: Duration,
42    pub(crate) db_max_lifetime: Duration,
43    pub(crate) db_statements_page_len: i32,
44
45    /// The base of this server's external URL as seen by its users.
46    pub external_url: String,
47    pub(crate) static_dir: PathBuf,
48    /// Mode of Operations + whether to enforce access authentication to LRS
49    /// resources.
50    pub mode: Mode,
51    pub(crate) root_email: String,
52    pub(crate) root_credentials: Option<u32>,
53    pub(crate) user_cache_len: NonZeroUsize,
54
55    pub(crate) ttl_batch_len: i32,
56    pub(crate) ttl: TimeDelta,
57    pub(crate) ttl_interval: u64,
58
59    pub(crate) mfc_interval: u64,
60
61    pub(crate) default_language: String,
62
63    /// Boolean flag that controls how a Statement's JWS signature is processed.
64    ///
65    /// When `false` a _Statement_ is deemed to be correcly signed if it's
66    /// _Equivalent_ to the one deserialized from the JWS Payload.
67    ///
68    /// When `true` and the JWS Header has an `x5c` property containing at least
69    /// one X.509 certificate, then a _Statement_ is deemed to be correctly
70    /// signed if additionally the certificates in the `x5c` array...
71    /// 1. Are time-valid at the time of processing the request,
72    /// 2. Each certificate's issuer's distinguished name matches the subject's
73    ///    distinguished name of the next certificate in the chain.
74    /// 3. Every certificate is signed by the next one.
75    /// 4. The JWS signature correctly matches the same generated using the RSA
76    ///    Public Key contained in the 1st certificate.
77    pub jws_strict: bool,
78}
79
80impl Default for Config {
81    fn default() -> Self {
82        let db_server_url = var("DB_SERVER_URL").expect("Missing DB_SERVERL_URL");
83        let db_name = var("DB_NAME").expect("Missing DB_NAME");
84
85        let db_max_connections: u32 = var("DB_MAX_CONNECTIONS")
86            .unwrap_or("8".to_string())
87            .parse()
88            .expect("Failed parsing DB_MAX_CONNECTIONS");
89        let db_min_connections: u32 = var("DB_MIN_CONNECTIONS")
90            .unwrap_or("4".to_string())
91            .parse()
92            .expect("Failed parsing DB_MIN_CONNECTIONS");
93        let db_acquire_timeout = Duration::from_secs(
94            var("DB_ACQUIRE_TIMEOUT_SECS")
95                .unwrap_or("8".to_string())
96                .parse()
97                .expect("Failed parsing DB_ACQUIRE_TIMEOUT_SECS"),
98        );
99        let db_idle_timeout = Duration::from_secs(
100            var("DB_IDLE_TIMEOUT_SECS")
101                .unwrap_or("8".to_string())
102                .parse()
103                .expect("Failed parsing DB_IDLE_TIMEOUT_SECS"),
104        );
105        let db_max_lifetime = Duration::from_secs(
106            var("DB_MAX_LIFETIME_SECS")
107                .unwrap_or("8".to_string())
108                .parse()
109                .expect("Failed parsing DB_MAX_LIFETIME_SECS"),
110        );
111
112        let db_statements_page_len: i32 = var("DB_STATEMENTS_PAGE_LEN")
113            .unwrap_or("20".to_string())
114            .parse()
115            .expect("Failed parsing DB_STATEMENTS_PAGE_LEN");
116        // ensure it's greater than 0 justin case...
117        assert!(
118            db_statements_page_len > 0,
119            "DB_STATEMENTS_PAGE_LEN must be greater than 0"
120        );
121
122        let mut external_url = var("LRS_EXTERNAL_URL").expect("Missing LRS_EXTERNAL_URL");
123        if external_url.ends_with(path::MAIN_SEPARATOR) {
124            external_url.pop();
125        }
126        let home_dir = my_home_dir();
127        let static_dir = Path::new(&home_dir).join("static").to_owned();
128
129        let mode: Mode = var("LRS_MODE")
130            .unwrap_or("legacy".to_owned())
131            .as_str()
132            .try_into()
133            .unwrap();
134        info!("*** LaRS will be running in {:?} mode", mode);
135        let root_email = match var("LRS_ROOT_EMAIL") {
136            Ok(x) => x,
137            Err(_) => match var("LRS_AUTHORITY_IFI") {
138                Ok(x) => {
139                    warn!("{}", DEPRECATION_MSG1);
140                    x
141                }
142                Err(_) => panic!(
143                    "Both LRS_ROOT_EMAIL and LRS_AUTHORITY_IFI are missing or contain invalid Unicode characters"
144                ),
145            },
146        };
147        // NOTE (rsn) 20250114 - raising an error when this env. var is missing
148        // forces admins of deployed instances, wishing to continue using LaRS
149        // in Legacy mode, to alter their setup for no added benefit.
150        // correct the documentation (and issue #5) to clarify this is now
151        // optional which in turn makes `root_credentials` Option<T>.
152        let root_credentials = match var("LRS_ROOT_PASSWORD") {
153            Ok(x) => {
154                let token = format!("{}:{}", root_email.as_str(), &x);
155                let encoded = BASE64_STANDARD.encode(token);
156                let hashed = fxhash::hash32(&encoded);
157                Some(hashed)
158            }
159            Err(_) => {
160                info!("Missing LRS_ROOT_PASSWORD. Will only operate in Legacy mode");
161                None
162            }
163        };
164        let user_cache_len = NonZeroUsize::new(
165            var("LRS_USER_CACHE_LEN")
166                .unwrap_or("100".to_string())
167                .parse()
168                .expect("Failed parsing LRS_USER_CACHE_LEN"),
169        )
170        .expect("Failed converting LRS_USER_CACHE_LEN to unsigned integer");
171        // notify sysadmin of LRS_AUTHORITY_IFI's deprecation...
172        if let Ok(x) = var("LRS_AUTHORITY_IFI") {
173            if x != root_email {
174                warn!("LRS_AUTHORITY_IFI is different than LRS_ROOT_EMAIL. Ignore + continue");
175            }
176            warn!("{}", DEPRECATION_MSG1);
177        }
178
179        // query filter views cache parameters...
180        let ttl_batch_len = i32::try_from(
181            var("TTL_BATCH_LEN")
182                .unwrap_or(DEFAULT_TTL_BATCH_LEN.to_string())
183                .parse::<u32>()
184                .expect("Failed parsing TTL_BATCH_LEN"),
185        )
186        .expect("Failed converting TTL_BATCH_LEN to i32");
187
188        let ttl_secs: usize = var("TTL_SECS")
189            .unwrap_or(DEFAULT_TTL_SECS.to_string())
190            .parse()
191            .expect("Failed parsing TTL_SECS");
192        let ttl = TimeDelta::new(
193            i64::try_from(ttl_secs).expect("Failed converting TTL_SECS to i64"),
194            0,
195        )
196        .expect("Failed converting TTL_SECS to TimeDelta");
197
198        let ttl_interval: u64 = var("TTL_INTERVAL_SECS")
199            .unwrap_or(DEFAULT_TTL_INTERVAL_SECS.to_string())
200            .parse()
201            .expect("Failed parsing TTL_INTERVAL_SECS");
202
203        let mfc_interval: u64 = var("MFC_INTERVAL_SECS")
204            .unwrap_or(DEFAULT_MFC_INTERVAL_SECS.to_string())
205            .parse()
206            .expect("Failed parsing MFC_INTERVAL_SECS");
207
208        let default_language = var("EXT_DEFAULT_LANGUAGE").expect("Missing EXT_DEFAULT_LANGUAGE");
209        // ensure it's valid...
210        let _ = MyLanguageTag::from_str(&default_language).expect("Invalid default language tag");
211
212        let jws_strict: bool = var("JWS_STRICT")
213            .unwrap_or("false".to_owned())
214            .parse()
215            .expect("Failed parsing JWS_STRICT");
216
217        Self {
218            db_server_url,
219            db_name,
220            db_max_connections,
221            db_min_connections,
222            db_acquire_timeout,
223            db_idle_timeout,
224            db_max_lifetime,
225            db_statements_page_len,
226            external_url,
227            static_dir,
228            mode,
229            root_email,
230            root_credentials,
231            user_cache_len,
232            ttl_batch_len,
233            ttl,
234            ttl_interval,
235            mfc_interval,
236            default_language,
237            jws_strict,
238        }
239    }
240}
241
242impl Config {
243    /// Construct a valid URL accessible externally (internet facing).
244    pub fn to_external_url(&self, partial: &str) -> String {
245        let mut url = self.external_url.clone();
246        if !partial.starts_with(path::MAIN_SEPARATOR) {
247            url.push(path::MAIN_SEPARATOR);
248        }
249        url.push_str(partial);
250        url
251    }
252
253    /// Return TRUE when running in legacy mode; FALSE otherwise.
254    pub fn is_legacy(&self) -> bool {
255        matches!(self.mode, Mode::Legacy)
256    }
257}
258
259fn my_home_dir() -> String {
260    let mut result = var("CARGO_MANIFEST_DIR").expect("Failed accessing Cargo vars...");
261    if result.ends_with(path::MAIN_SEPARATOR) {
262        result.pop();
263    }
264    result
265}