mirror of
https://github.com/rzuasti/oott.git
synced 2026-07-08 19:21:54 +02:00
Add an ArpScanner Default impl (30m/1m/10m, enabled) and mark the arp_scanner field with serde default, so the [arp_scanner] section can now be omitted entirely and fall back to code defaults — matching the pattern used by the SNMP scanner. Previously these three durations were mandatory and the backend would not start without them. Reconcile the documentation to the canonical 30m/1m/10m: fix the README NixOS example (was 15m/20m/30m) and options table (was 15m), and note that the section is optional in both the README and sample TOML. The NixOS module and sample TOML already used these values. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
335 lines
9.1 KiB
Rust
335 lines
9.1 KiB
Rust
use config::{Config, ConfigError, File};
|
|
use duration_string::DurationString;
|
|
use once_cell::sync::OnceCell;
|
|
use serde::Deserialize;
|
|
|
|
// -----------------------------------------------------------
|
|
// Configuration structure
|
|
|
|
fn default_true() -> bool {
|
|
true
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, Clone)]
|
|
pub struct Database {
|
|
pub path: String,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, Clone)]
|
|
pub struct Networking {
|
|
pub interface: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, Clone)]
|
|
pub struct Log {
|
|
pub level: String,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, Clone)]
|
|
pub struct ArpScanner {
|
|
#[serde(default = "default_true")]
|
|
pub enabled: bool,
|
|
pub wait_between_scans: DurationString,
|
|
pub sender_timeout: DurationString,
|
|
pub scan_duration: DurationString,
|
|
}
|
|
|
|
impl Default for ArpScanner {
|
|
fn default() -> Self {
|
|
ArpScanner {
|
|
enabled: true,
|
|
wait_between_scans: DurationString::try_from("30m".to_string()).unwrap(),
|
|
sender_timeout: DurationString::try_from("1m".to_string()).unwrap(),
|
|
scan_duration: DurationString::try_from("10m".to_string()).unwrap(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, Clone)]
|
|
pub struct MdnsScanner {
|
|
#[serde(default = "default_true")]
|
|
pub enabled: bool,
|
|
pub probe_timeout: DurationString,
|
|
}
|
|
|
|
impl Default for MdnsScanner {
|
|
fn default() -> Self {
|
|
MdnsScanner {
|
|
enabled: true,
|
|
probe_timeout: DurationString::try_from("2s".to_string()).unwrap(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, Clone)]
|
|
pub struct SsdpScanner {
|
|
#[serde(default = "default_true")]
|
|
pub enabled: bool,
|
|
pub probe_timeout: DurationString,
|
|
}
|
|
|
|
impl Default for SsdpScanner {
|
|
fn default() -> Self {
|
|
SsdpScanner {
|
|
enabled: true,
|
|
probe_timeout: DurationString::try_from("2s".to_string()).unwrap(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, Clone)]
|
|
pub struct DhcpScanner {
|
|
#[serde(default = "default_true")]
|
|
pub enabled: bool,
|
|
}
|
|
|
|
impl Default for DhcpScanner {
|
|
fn default() -> Self {
|
|
DhcpScanner { enabled: true }
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, Clone)]
|
|
pub struct SnmpScanner {
|
|
#[serde(default = "default_true")]
|
|
pub enabled: bool,
|
|
/// SNMP agent to poll, as `host:port` (e.g. the gateway: `192.168.1.1:161`).
|
|
pub target: String,
|
|
/// SNMPv2c read-only community string.
|
|
pub community: String,
|
|
pub wait_between_scans: DurationString,
|
|
pub timeout: DurationString,
|
|
}
|
|
|
|
impl Default for SnmpScanner {
|
|
fn default() -> Self {
|
|
// No universal target exists, so the SNMP scanner stays off until the user adds a
|
|
// `[snmp_scanner]` section pointing at their gateway.
|
|
SnmpScanner {
|
|
enabled: false,
|
|
target: String::new(),
|
|
community: String::new(),
|
|
wait_between_scans: DurationString::try_from("10m".to_string()).unwrap(),
|
|
timeout: DurationString::try_from("5s".to_string()).unwrap(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, Clone)]
|
|
pub struct Pushover {
|
|
pub token: String,
|
|
pub user_key: String,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, Clone)]
|
|
pub struct Notifications {
|
|
pub method: String,
|
|
pub pushover: Pushover,
|
|
pub notify_when_not_seen_for: DurationString,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, Clone)]
|
|
pub struct WebServer {
|
|
pub ip_address: String,
|
|
pub port: u16,
|
|
pub api_key: String,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, Clone)]
|
|
pub struct Retention {
|
|
pub window: DurationString,
|
|
}
|
|
|
|
impl Default for Retention {
|
|
fn default() -> Self {
|
|
Retention {
|
|
window: DurationString::try_from("365d".to_string()).unwrap(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, Clone)]
|
|
pub struct Settings {
|
|
pub database: Database,
|
|
pub networking: Networking,
|
|
pub log: Log,
|
|
#[serde(default)]
|
|
pub arp_scanner: ArpScanner,
|
|
pub notifications: Notifications,
|
|
pub web_server: WebServer,
|
|
#[serde(default)]
|
|
pub retention: Retention,
|
|
#[serde(default)]
|
|
pub mdns_scanner: MdnsScanner,
|
|
#[serde(default)]
|
|
pub ssdp_scanner: SsdpScanner,
|
|
#[serde(default)]
|
|
pub dhcp_scanner: DhcpScanner,
|
|
#[serde(default)]
|
|
pub snmp_scanner: SnmpScanner,
|
|
}
|
|
// End configuration structure
|
|
// -----------------------------------------------------------
|
|
|
|
impl Settings {
|
|
pub fn new(config_path: String) -> Result<Self, ConfigError> {
|
|
println!("Reading configuration from {}", config_path);
|
|
|
|
let local_settings = Config::builder()
|
|
.add_source(File::with_name(config_path.as_str()))
|
|
.build()?;
|
|
|
|
local_settings.try_deserialize()
|
|
}
|
|
}
|
|
|
|
pub const DEFAULT_CONFIG_FILE_PATH: &str = "./oott.toml";
|
|
static SETTINGS: OnceCell<Settings> = OnceCell::new();
|
|
|
|
pub fn get_settings() -> &'static Settings {
|
|
match SETTINGS.get() {
|
|
Some(value) => value,
|
|
None => {
|
|
println!(
|
|
"Configuration was not initialized, reading from default path ({})",
|
|
DEFAULT_CONFIG_FILE_PATH
|
|
);
|
|
let settings = Settings::new(DEFAULT_CONFIG_FILE_PATH.to_string()).unwrap();
|
|
let _ = SETTINGS.set(settings);
|
|
SETTINGS.get().unwrap()
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn init(config_path: String) {
|
|
let _ = SETTINGS.set(Settings::new(config_path).unwrap());
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use config::{Config, FileFormat};
|
|
|
|
fn parse(toml: &str) -> Settings {
|
|
Config::builder()
|
|
.add_source(config::File::from_str(toml, FileFormat::Toml))
|
|
.build()
|
|
.unwrap()
|
|
.try_deserialize()
|
|
.unwrap()
|
|
}
|
|
|
|
const BASE_CONFIG: &str = r#"
|
|
[database]
|
|
path = "./oott.db"
|
|
[networking]
|
|
[log]
|
|
level = "info"
|
|
[arp_scanner]
|
|
wait_between_scans = "1m"
|
|
sender_timeout = "1m"
|
|
scan_duration = "2m"
|
|
[notifications]
|
|
method = "none"
|
|
notify_when_not_seen_for = "1w"
|
|
[notifications.pushover]
|
|
token = ""
|
|
user_key = ""
|
|
[web_server]
|
|
ip_address = "0.0.0.0"
|
|
port = 3000
|
|
api_key = "test"
|
|
"#;
|
|
|
|
#[test]
|
|
fn scanners_enabled_by_default_when_flag_omitted() {
|
|
let settings = parse(BASE_CONFIG);
|
|
assert!(settings.arp_scanner.enabled);
|
|
assert!(settings.mdns_scanner.enabled);
|
|
assert!(settings.ssdp_scanner.enabled);
|
|
assert!(settings.dhcp_scanner.enabled);
|
|
// The SNMP scanner is opt-in: with no `[snmp_scanner]` section it stays disabled.
|
|
assert!(!settings.snmp_scanner.enabled);
|
|
}
|
|
|
|
#[test]
|
|
fn snmp_scanner_section_is_parsed() {
|
|
let toml = format!(
|
|
"{BASE_CONFIG}
|
|
[snmp_scanner]
|
|
target = \"192.168.1.1:161\"
|
|
community = \"public\"
|
|
wait_between_scans = \"5m\"
|
|
timeout = \"3s\"
|
|
"
|
|
);
|
|
let settings = parse(&toml);
|
|
// `enabled` defaults to true once the section is present.
|
|
assert!(settings.snmp_scanner.enabled);
|
|
assert_eq!(settings.snmp_scanner.target, "192.168.1.1:161");
|
|
assert_eq!(settings.snmp_scanner.community, "public");
|
|
}
|
|
|
|
#[test]
|
|
fn arp_scanner_uses_code_defaults_when_section_omitted() {
|
|
// A config without an `[arp_scanner]` section falls back to the code defaults.
|
|
const NO_ARP_CONFIG: &str = r#"
|
|
[database]
|
|
path = "./oott.db"
|
|
[networking]
|
|
[log]
|
|
level = "info"
|
|
[notifications]
|
|
method = "none"
|
|
notify_when_not_seen_for = "1w"
|
|
[notifications.pushover]
|
|
token = ""
|
|
user_key = ""
|
|
[web_server]
|
|
ip_address = "0.0.0.0"
|
|
port = 3000
|
|
api_key = "test"
|
|
"#;
|
|
let settings = parse(NO_ARP_CONFIG);
|
|
assert!(settings.arp_scanner.enabled);
|
|
assert_eq!(
|
|
std::time::Duration::from(settings.arp_scanner.wait_between_scans),
|
|
std::time::Duration::from_secs(30 * 60)
|
|
);
|
|
assert_eq!(
|
|
std::time::Duration::from(settings.arp_scanner.sender_timeout),
|
|
std::time::Duration::from_secs(60)
|
|
);
|
|
assert_eq!(
|
|
std::time::Duration::from(settings.arp_scanner.scan_duration),
|
|
std::time::Duration::from_secs(10 * 60)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn scanners_can_be_disabled() {
|
|
let toml = format!(
|
|
"{BASE_CONFIG}
|
|
[mdns_scanner]
|
|
enabled = false
|
|
probe_timeout = \"2s\"
|
|
[ssdp_scanner]
|
|
enabled = false
|
|
probe_timeout = \"2s\"
|
|
[dhcp_scanner]
|
|
enabled = false
|
|
"
|
|
);
|
|
// Disable the ARP scanner via its existing section too.
|
|
let toml = toml.replace(
|
|
"wait_between_scans = \"1m\"",
|
|
"enabled = false\n wait_between_scans = \"1m\"",
|
|
);
|
|
let settings = parse(&toml);
|
|
assert!(!settings.arp_scanner.enabled);
|
|
assert!(!settings.mdns_scanner.enabled);
|
|
assert!(!settings.ssdp_scanner.enabled);
|
|
assert!(!settings.dhcp_scanner.enabled);
|
|
}
|
|
}
|