evergreen/osrf/
conf.rs

1use gethostname::gethostname;
2use roxmltree;
3use std::fmt;
4use std::fs;
5use std::str::FromStr;
6use std::sync::OnceLock;
7use syslog;
8
9static GLOBAL_OSRF_CONFIG: OnceLock<Config> = OnceLock::new();
10
11/// Returns a ref to the globab OpenSRF config.
12///
13/// Panics if no configuration has been loaded.
14pub fn config() -> &'static Config {
15    if let Some(conf) = GLOBAL_OSRF_CONFIG.get() {
16        conf
17    } else {
18        log::error!("OpenSRF Config Required");
19        panic!("OpenSRF Config Required")
20    }
21}
22
23const DEFAULT_BUS_PORT: u16 = 6379;
24
25#[derive(Debug, Clone, PartialEq)]
26pub enum LogFile {
27    Syslog,
28    Stdout,
29    Filename(String),
30}
31
32#[derive(Debug, Clone)]
33pub struct LogOptions {
34    log_level: Option<log::LevelFilter>,
35    log_file: Option<LogFile>,
36    syslog_facility: Option<syslog::Facility>,
37    activity_log_facility: Option<syslog::Facility>,
38}
39
40impl LogOptions {
41    pub fn syslog_facility(&self) -> Option<syslog::Facility> {
42        self.syslog_facility
43    }
44    pub fn set_syslog_facility(&mut self, facility: &str) -> Result<(), String> {
45        if let Ok(ff) = syslog::Facility::from_str(facility) {
46            self.syslog_facility = Some(ff);
47            Ok(())
48        } else {
49            Err(format!("Invalid syslog facility string: {facility}"))
50        }
51    }
52
53    pub fn activity_log_facility(&self) -> Option<syslog::Facility> {
54        self.activity_log_facility
55    }
56    pub fn log_file(&self) -> &Option<LogFile> {
57        &self.log_file
58    }
59    pub fn set_log_file(&mut self, file: LogFile) {
60        self.log_file = Some(file);
61    }
62    pub fn log_level(&self) -> &Option<log::LevelFilter> {
63        &self.log_level
64    }
65    pub fn set_log_level(&mut self, level: &str) {
66        self.log_level = Some(LogOptions::log_level_from_str(level));
67    }
68
69    /// Maps log levels as defined in the OpenSRF core configuration
70    /// file to syslog levels.
71    ///
72    /// Defaults to Info
73    pub fn log_level_from_str(level: &str) -> log::LevelFilter {
74        match level {
75            "1" | "error" => log::LevelFilter::Error,
76            "2" | "warn" => log::LevelFilter::Warn,
77            "3" | "info" => log::LevelFilter::Info,
78            "4" | "debug" => log::LevelFilter::Debug,
79            "5" | "trace" => log::LevelFilter::Trace,
80            _ => log::LevelFilter::Info,
81        }
82    }
83}
84
85/// A single message bus endpoint domain/host.
86#[derive(Debug, Clone)]
87pub struct BusDomain {
88    name: String,
89    port: u16,
90}
91
92impl BusDomain {
93    pub fn name(&self) -> &str {
94        &self.name
95    }
96    pub fn port(&self) -> u16 {
97        self.port
98    }
99}
100
101impl fmt::Display for BusDomain {
102    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
103        write!(f, "{}:{}", self.name, self.port)
104    }
105}
106
107/// A set of bus login credentials
108#[derive(Debug, Clone)]
109pub struct BusClient {
110    username: String,
111    password: String,
112    router_name: String,
113    domain: BusDomain,
114    logging: LogOptions,
115    settings_config: Option<String>,
116    routers: Vec<ClientRouter>,
117}
118
119impl BusClient {
120    pub fn username(&self) -> &str {
121        &self.username
122    }
123    pub fn password(&self) -> &str {
124        &self.password
125    }
126    pub fn domain(&self) -> &BusDomain {
127        &self.domain
128    }
129    /// Name of the router running on our domain.
130    pub fn router_name(&self) -> &str {
131        &self.router_name
132    }
133    pub fn logging(&self) -> &LogOptions {
134        &self.logging
135    }
136    pub fn logging_mut(&mut self) -> &mut LogOptions {
137        &mut self.logging
138    }
139    pub fn settings_config(&self) -> Option<&str> {
140        self.settings_config.as_deref()
141    }
142    pub fn routers(&self) -> &Vec<ClientRouter> {
143        &self.routers
144    }
145    pub fn set_domain(&mut self, domain: &str) {
146        // Assumes other aspects of the domain are identical
147        self.domain.name = domain.to_string();
148    }
149    pub fn set_username(&mut self, username: &str) {
150        self.username = username.to_string();
151    }
152    pub fn set_password(&mut self, password: &str) {
153        self.password = password.to_string();
154    }
155}
156
157#[derive(Debug, Clone)]
158pub struct ClientRouter {
159    domain: String,
160    username: String,
161    services: Option<Vec<String>>,
162}
163impl ClientRouter {
164    pub fn services(&self) -> Option<&Vec<String>> {
165        self.services.as_ref()
166    }
167    pub fn domain(&self) -> &str {
168        &self.domain
169    }
170    /// "name" in opensrf_core.xml
171    pub fn username(&self) -> &str {
172        &self.username
173    }
174}
175
176#[derive(Debug, Clone)]
177pub struct Router {
178    client: BusClient,
179    trusted_server_domains: Vec<String>,
180    trusted_client_domains: Vec<String>,
181}
182
183impl Router {
184    pub fn client(&self) -> &BusClient {
185        &self.client
186    }
187    pub fn client_mut(&mut self) -> &mut BusClient {
188        &mut self.client
189    }
190    pub fn trusted_server_domains(&self) -> &Vec<String> {
191        &self.trusted_server_domains
192    }
193    pub fn trusted_client_domains(&self) -> &Vec<String> {
194        &self.trusted_client_domains
195    }
196}
197
198#[derive(Debug, Clone)]
199pub struct ConfigBuilder {
200    client: Option<BusClient>,
201    routers: Vec<Router>,
202    gateway: Option<BusClient>,
203    log_protect: Vec<String>,
204}
205
206impl ConfigBuilder {
207    pub fn build(self) -> Result<Config, String> {
208        if self.client.is_none() {
209            return Err("Config has no client settings".to_string());
210        }
211
212        Ok(Config {
213            hostname: Config::get_os_hostname()?,
214            client: self.client.unwrap(),
215            routers: self.routers,
216            gateway: self.gateway,
217            log_protect: self.log_protect,
218        })
219    }
220
221    /// Load configuration from a YAML file.
222    ///
223    /// May panic on invalid values (e.g. invalid log level) or unexpected
224    /// Yaml config structures.
225    pub fn from_file(filename: &str) -> Result<Self, String> {
226        match fs::read_to_string(filename) {
227            Ok(text) => ConfigBuilder::from_xml_string(&text),
228            Err(e) => Err(format!(
229                "Error reading configuration file: file='{}' {:?}",
230                filename, e
231            )),
232        }
233    }
234
235    pub fn from_xml_string(xml: &str) -> Result<Self, String> {
236        let doc = roxmltree::Document::parse(xml).map_err(|e| format!("Error parsing XML: {e}"))?;
237
238        let conf_node = match doc.root().children().find(|n| n.has_tag_name("config")) {
239            Some(n) => n,
240            None => Err("Missing 'config' element".to_string())?,
241        };
242
243        let mut builder = ConfigBuilder {
244            client: None,
245            gateway: None,
246            routers: Vec::new(),
247            log_protect: Vec::new(),
248        };
249
250        // Start with the Client portion, which will contain values
251        // for all connections.
252        for node in conf_node.children() {
253            match node.tag_name().name() {
254                "opensrf" => builder.unpack_opensrf_node(&node)?,
255                "routers" => builder.unpack_routers(&node)?,
256                "gateway" => builder.unpack_gateway(&node)?,
257                "shared" => builder.unpack_shared(&node)?,
258                _ => {} // ignore
259            }
260        }
261
262        Ok(builder)
263    }
264
265    fn unpack_gateway(&mut self, node: &roxmltree::Node) -> Result<(), String> {
266        self.gateway = Some(self.unpack_client_node(node)?);
267        Ok(())
268    }
269
270    fn unpack_shared(&mut self, node: &roxmltree::Node) -> Result<(), String> {
271        if let Some(lp) = node.children().find(|c| c.has_tag_name("log_protect")) {
272            for ms in lp.children().filter(|c| c.has_tag_name("match_string")) {
273                if let Some(t) = ms.text() {
274                    self.log_protect.push(t.to_string());
275                }
276            }
277        }
278
279        Ok(())
280    }
281
282    fn unpack_routers(&mut self, node: &roxmltree::Node) -> Result<(), String> {
283        for rnode in node.children().filter(|n| n.has_tag_name("router")) {
284            // Router client configs are (mostly) nested in a <transport> element.
285            let tnode = match rnode.children().find(|c| c.has_tag_name("transport")) {
286                Some(tn) => tn,
287                None => Err("Routers require a transport config".to_string())?,
288            };
289
290            let mut client = self.unpack_client_node(&tnode)?;
291
292            // The logging configs for the routers sits outside its
293            // transport node.
294            client.logging = self.unpack_logging_node(&rnode)?;
295
296            let mut router = Router {
297                client,
298                trusted_server_domains: Vec::new(),
299                trusted_client_domains: Vec::new(),
300            };
301
302            for tdnode in rnode
303                .children()
304                .filter(|d| d.has_tag_name("trusted_domains"))
305            {
306                for snode in tdnode.children().filter(|d| d.has_tag_name("server")) {
307                    if let Some(domain) = snode.text() {
308                        router.trusted_server_domains.push(domain.to_string());
309                    }
310                }
311                for cnode in tdnode.children().filter(|d| d.has_tag_name("client")) {
312                    if let Some(domain) = cnode.text() {
313                        router.trusted_client_domains.push(domain.to_string());
314                    }
315                }
316            }
317
318            self.routers.push(router);
319        }
320
321        Ok(())
322    }
323
324    fn child_node_text(&self, node: &roxmltree::Node, name: &str) -> Option<String> {
325        if let Some(tnode) = node.children().find(|n| n.has_tag_name(name)) {
326            if let Some(text) = tnode.text() {
327                return Some(text.to_string());
328            }
329        }
330        None
331    }
332
333    fn unpack_opensrf_node(&mut self, node: &roxmltree::Node) -> Result<(), String> {
334        let mut client = self.unpack_client_node(node)?;
335
336        if let Some(routers) = node.children().find(|c| c.has_tag_name("routers")) {
337            for rnode in routers.children().filter(|r| r.has_tag_name("router")) {
338                self.unpack_client_router_node(&mut client, &rnode)?;
339            }
340        }
341
342        self.client = Some(client);
343
344        Ok(())
345    }
346
347    fn unpack_client_router_node(
348        &mut self,
349        client: &mut BusClient,
350        rnode: &roxmltree::Node,
351    ) -> Result<(), String> {
352        let domain = match self.child_node_text(rnode, "domain") {
353            Some(d) => d.to_string(),
354            None => Err(format!("Client router node has no domain: {rnode:?}"))?,
355        };
356
357        let username = match self.child_node_text(rnode, "name") {
358            Some(d) => d.to_string(),
359            None => Err(format!("Client router node has no name: {rnode:?}"))?,
360        };
361
362        let mut cr = ClientRouter {
363            domain,
364            username,
365            services: None,
366        };
367
368        if let Some(services) = rnode.children().find(|n| n.has_tag_name("services")) {
369            let mut svclist = Vec::new();
370
371            for snode in services.children().filter(|n| n.has_tag_name("service")) {
372                if let Some(service) = snode.text() {
373                    svclist.push(service.to_string());
374                }
375            }
376
377            cr.services = Some(svclist);
378        }
379
380        client.routers.push(cr);
381
382        Ok(())
383    }
384
385    fn unpack_client_node(&mut self, node: &roxmltree::Node) -> Result<BusClient, String> {
386        let logging = self.unpack_logging_node(node)?;
387        let domain = self.unpack_domain_node(node)?;
388
389        let mut username = "";
390        let mut password = "";
391        let mut router_name = "router";
392        let mut settings_config: Option<String> = None;
393
394        for child in node.children() {
395            match child.tag_name().name() {
396                "username" => {
397                    if let Some(t) = child.text() {
398                        username = t;
399                    }
400                }
401                "passwd" | "password" => {
402                    if let Some(t) = child.text() {
403                        password = t;
404                    }
405                }
406                "router_name" => {
407                    if let Some(t) = child.text() {
408                        router_name = t;
409                    }
410                }
411                "settings_config" => {
412                    if let Some(t) = child.text() {
413                        settings_config = Some(t.to_string());
414                    }
415                }
416                _ => {}
417            }
418        }
419
420        Ok(BusClient {
421            domain,
422            logging,
423            settings_config,
424            routers: Vec::new(),
425            username: username.to_string(),
426            password: password.to_string(),
427            router_name: router_name.to_string(),
428        })
429    }
430
431    fn unpack_domain_node(&mut self, node: &roxmltree::Node) -> Result<BusDomain, String> {
432        let domain_name = match node.children().find(|c| c.has_tag_name("domain")) {
433            Some(n) => match n.text() {
434                Some(t) => t,
435                None => return Err("'domain' node is empty".to_string()),
436            },
437            None => match node.children().find(|c| c.has_tag_name("server")) {
438                Some(n) => match n.text() {
439                    Some(t) => t,
440                    None => return Err("'server' node is empty".to_string()),
441                },
442                None => return Err("Node has no domain or server".to_string()),
443            },
444        };
445
446        let mut port = DEFAULT_BUS_PORT;
447        if let Some(pnode) = node.children().find(|c| c.has_tag_name("port")) {
448            if let Some(ptext) = pnode.text() {
449                if let Ok(p) = ptext.parse::<u16>() {
450                    port = p;
451                }
452            }
453        }
454
455        Ok(BusDomain {
456            port,
457            name: domain_name.to_string(),
458        })
459    }
460
461    fn unpack_logging_node(&mut self, node: &roxmltree::Node) -> Result<LogOptions, String> {
462        let mut ops = LogOptions {
463            log_level: None,
464            log_file: None,
465            syslog_facility: None,
466            activity_log_facility: None,
467        };
468
469        for child in node.children() {
470            match child.tag_name().name() {
471                "logfile" => {
472                    if let Some(filename) = child.text() {
473                        if filename.eq("syslog") {
474                            ops.log_file = Some(LogFile::Syslog);
475                        } else if filename.eq("stdout") {
476                            ops.log_file = Some(LogFile::Stdout);
477                        } else {
478                            ops.log_file = Some(LogFile::Filename(filename.to_string()))
479                        }
480                    }
481                }
482                "syslog" => {
483                    if let Some(f) = child.text() {
484                        if let Ok(ff) = syslog::Facility::from_str(f) {
485                            ops.syslog_facility = Some(ff);
486                        }
487                    }
488                }
489                "actlog" => {
490                    if let Some(f) = child.text() {
491                        if let Ok(ff) = syslog::Facility::from_str(f) {
492                            ops.activity_log_facility = Some(ff);
493                        }
494                    }
495                }
496                "loglevel" => {
497                    if let Some(level_num) = child.text() {
498                        ops.log_level = Some(LogOptions::log_level_from_str(level_num));
499                    }
500                }
501                _ => {}
502            }
503        }
504
505        Ok(ops)
506    }
507}
508
509#[derive(Debug, Clone)]
510pub struct Config {
511    hostname: String,
512    client: BusClient,
513    routers: Vec<Router>,
514    gateway: Option<BusClient>,
515    log_protect: Vec<String>,
516}
517
518impl Config {
519    /// Put this Config into the global GLOBAL_OSRF_CONFIG.
520    ///
521    /// Returns Err if the Config has already been stored.
522    pub fn store(self) -> Result<(), String> {
523        if GLOBAL_OSRF_CONFIG.set(self).is_err() {
524            Err("Cannot initialize OpenSRF Config more than once".to_string())
525        } else {
526            Ok(())
527        }
528    }
529
530    pub fn routers(&self) -> &Vec<Router> {
531        &self.routers
532    }
533    pub fn routers_mut(&mut self) -> Vec<&mut Router> {
534        self.routers.iter_mut().collect()
535    }
536
537    pub fn log_protect(&self) -> &Vec<String> {
538        &self.log_protect
539    }
540
541    pub fn gateway(&self) -> Option<&BusClient> {
542        self.gateway.as_ref()
543    }
544    pub fn gateway_mut(&mut self) -> Option<&mut BusClient> {
545        self.gateway.as_mut()
546    }
547
548    pub fn client(&self) -> &BusClient {
549        &self.client
550    }
551    pub fn client_mut(&mut self) -> &mut BusClient {
552        &mut self.client
553    }
554    pub fn hostname(&self) -> &str {
555        &self.hostname
556    }
557
558    pub fn get_router_conf(&self, domain: &str) -> Option<&Router> {
559        self.routers
560            .iter()
561            .find(|r| r.client().domain().name().eq(domain))
562    }
563
564    /// Manually override the OS hostname, e.g. with "localhost"
565    pub fn set_hostname(&mut self, hostname: &str) {
566        self.hostname = hostname.to_string();
567    }
568
569    fn get_os_hostname() -> Result<String, String> {
570        match gethostname().into_string() {
571            Ok(h) => Ok(h),
572            Err(e) => Err(format!("Cannot read OS host name: {e:?}")),
573        }
574    }
575}