This commit is contained in:
Crom (Thibaut CHARLES) 2019-02-14 08:21:22 +01:00
parent d030beb00b
commit 4f1f1ba88e
Signed by: tcharles
GPG Key ID: 45A3D5F880B9E6D0
24 changed files with 4131 additions and 250 deletions

35
Cargo.lock generated
View File

@ -1,3 +1,5 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
[[package]]
name = "aho-corasick"
version = "0.6.9"
@ -15,6 +17,11 @@ dependencies = [
"hex 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "byteorder"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "cc"
version = "1.0.28"
@ -216,16 +223,20 @@ dependencies = [
"getopts 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)",
"globset 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"libloading 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
"regex 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.85 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_derive 1.0.85 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_yaml 0.8.8 (registry+https://github.com/rust-lang/crates.io-index)",
"simplelog 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "serde"
version = "1.0.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"serde_derive 1.0.85 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "serde_derive"
@ -248,6 +259,16 @@ dependencies = [
"yaml-rust 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "simplelog"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"chrono 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
"term 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "syn"
version = "0.15.26"
@ -258,6 +279,15 @@ dependencies = [
"unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "term"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "thread_local"
version = "0.3.6"
@ -326,6 +356,7 @@ dependencies = [
[metadata]
"checksum aho-corasick 0.6.9 (registry+https://github.com/rust-lang/crates.io-index)" = "1e9a933f4e58658d7b12defcf96dc5c720f20832deebe3e0a19efd3b6aaeeb9e"
"checksum blurz 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b1bc2df4d0f1d373b324672d9966aca3f5ba1f03d69edad6240144774539ea59"
"checksum byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a019b10a2a7cdeb292db131fc8113e57ea2a908f6e7894b0c3c671893b65dbeb"
"checksum cc 1.0.28 (registry+https://github.com/rust-lang/crates.io-index)" = "bb4a8b715cb4597106ea87c7c84b2f1d452c7492033765df7f32651e66fcf749"
"checksum cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "082bb9b28e00d3c9d39cc03e64ce4cea0f1bb9b3fde493f0cbc008472d22bdf4"
"checksum chrono 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "45912881121cb26fad7c38c17ba7daa18764771836b34fab7d3fbd93ed633878"
@ -355,7 +386,9 @@ dependencies = [
"checksum serde 1.0.85 (registry+https://github.com/rust-lang/crates.io-index)" = "534b8b91a95e0f71bca3ed5824752d558da048d4248c91af873b63bd60519752"
"checksum serde_derive 1.0.85 (registry+https://github.com/rust-lang/crates.io-index)" = "a915306b0f1ac5607797697148c223bedeaa36bcc2e28a01441cd638cc6567b4"
"checksum serde_yaml 0.8.8 (registry+https://github.com/rust-lang/crates.io-index)" = "0887a8e097a69559b56aa2526bf7aff7c3048cf627dff781f0b56a6001534593"
"checksum simplelog 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2e95345f185d5adeb8ec93459d2dc99654e294cc6ccf5b75414d8ea262de9a13"
"checksum syn 0.15.26 (registry+https://github.com/rust-lang/crates.io-index)" = "f92e629aa1d9c827b2bb8297046c1ccffc57c99b947a680d3ccff1f136a3bee9"
"checksum term 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "5e6b677dd1e8214ea1ef4297f85dbcbed8e8cdddb561040cc998ca2551c37561"
"checksum thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c6b53e329000edc2b34dbe8545fd20e55a333362d0a321909685a19bd28c3f1b"
"checksum time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "db8dcfca086c1143c9270ac42a2bbd8a7ee477b78ac8e45b19abfb0cbede4b6f"
"checksum ucd-util 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "535c204ee4d8434478593480b8f86ab45ec9aae0e83c568ca81abf0fd0e88f86"

View File

@ -6,11 +6,12 @@ edition = "2018"
[dependencies]
libloading = "0.5.0"
serde = "1.0"
serde = { version = "1.0", features = ["derive"] }
serde_yaml = "0.8"
serde_derive = "1.0"
getopts = "0.2"
blurz = "0.4.0"
regex = "1"
chrono = "0.4"
globset = "0.3"
log = "0.4"
simplelog = "0.5"

View File

@ -1,8 +0,0 @@
use serde_derive::*;
use std::collections::HashMap;
#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct Config {
pub monitors: Vec<HashMap<String, serde_yaml::Value>>,
pub outputs: Vec<HashMap<String, serde_yaml::Value>>,
}

83
src/filter.rs Normal file
View File

@ -0,0 +1,83 @@
use globset::{Glob, GlobMatcher};
pub use std::collections::HashMap;
use log::{info, trace, warn};
use crate::message::*;
#[derive(Debug)]
pub struct Filter {
level: Level,
include_types: Vec<GlobMatcher>,
exclude_types: Vec<GlobMatcher>,
}
impl Filter {
pub fn new(config: &HashMap<String, serde_yaml::Value>) -> Self {
let filter_node = config
.get("filter")
.unwrap_or(&serde_yaml::Mapping::new().into())
.clone();
let include_types = match filter_node.get("include_types") {
Some(v) => v
.as_sequence()
.expect("include_types must be a list of strings")
.into_iter()
.map(|x| {
Glob::new(x.as_str().expect("filter must be a list of strings"))
.expect("Invalid glob pattern")
.compile_matcher()
})
.collect(),
None => vec![],
};
let exclude_types = match filter_node.get("exclude_types") {
Some(v) => v
.as_sequence()
.expect("exclude_types must be a list of strings")
.into_iter()
.map(|x| {
Glob::new(x.as_str().expect("filter must be a list of strings"))
.expect("Invalid glob pattern")
.compile_matcher()
})
.collect(),
None => vec![],
};
let level = match filter_node.get("level") {
Some(l) => Level::try_from(l.as_str().expect("Level must be a string"))
.expect("Unknown level name"),
None => Level::Notice,
};
Filter {
level: level,
include_types: include_types,
exclude_types: exclude_types,
}
}
pub fn is_message_allowed(&self, message: &Message) -> bool {
if message.level < self.level {
return false;
}
for exc in &self.exclude_types {
if exc.is_match(&message.msg_type) {
return false;
}
}
if self.include_types.len() > 0 {
for exc in &self.include_types {
if exc.is_match(&message.msg_type) {
return true;
}
}
return false;
} else {
return true;
}
}
}

View File

@ -1,25 +1,37 @@
mod config;
#![feature(unrestricted_attribute_tokens)]
#![feature(custom_attribute)]
#![feature(try_from)]
mod filter;
mod message;
mod monitor;
mod monitors;
mod output;
mod plugins;
mod outputs;
extern crate getopts;
extern crate serde_derive;
extern crate globset;
extern crate log;
extern crate serde_yaml;
extern crate simplelog;
use getopts::Options;
use serde::Deserialize;
use std::collections::HashMap;
use std::env;
use std::fs::File;
use std::io::prelude::*;
use std::sync::mpsc;
use std::thread;
use getopts::Options;
use crate::config::*;
use crate::filter::*;
use crate::message::*;
use crate::monitor::*;
use crate::output::*;
#[derive(Debug, Deserialize)]
pub struct AppConfig {
pub monitors: Vec<HashMap<String, serde_yaml::Value>>,
pub outputs: Vec<HashMap<String, serde_yaml::Value>>,
}
fn main() {
let args: Vec<String> = env::args().collect();
@ -33,6 +45,8 @@ fn main() {
"config.yaml",
);
opts.optflag("h", "help", "print this help menu");
opts.optflag("v", "verbose", "Switch to DEBUG output");
opts.optflag("q", "quiet", "Switch to WARNING output");
let matches = match opts.parse(&args[1..]) {
Ok(m) => m,
@ -44,6 +58,30 @@ fn main() {
return;
}
let level;
if matches.opt_present("v") && matches.opt_present("q") {
panic!("-v and -q are mutually exclusive");
}
if matches.opt_present("v") {
level = simplelog::LevelFilter::Debug;
} else if matches.opt_present("q") {
level = simplelog::LevelFilter::Warn;
} else {
level = simplelog::LevelFilter::Info;
}
simplelog::TermLogger::init(
level,
simplelog::Config {
time: Some(simplelog::Level::Debug),
level: Some(simplelog::Level::Error),
target: Some(simplelog::Level::Info),
location: Some(simplelog::Level::Debug),
time_format: None,
},
)
.unwrap();
// Parse config
let config_path = matches.opt_str("c").unwrap_or("./config.yaml".to_owned());
@ -52,7 +90,7 @@ fn main() {
config_file
.read_to_string(&mut config_content)
.expect("Could not read configuration file");
let config: Config =
let config: AppConfig =
serde_yaml::from_str(&config_content).expect("Invalid config file content");
// Monitors -> dispatcher channel
@ -60,40 +98,53 @@ fn main() {
// Instantiate monitor threads and structs
for mon_config in config.monitors {
let name = mon_config
.get("name")
.expect("Missing `name` key for monitor")
let mon_type = mon_config
.get("type")
.expect("Missing `type` key for monitor")
.as_str()
.expect("Key `name` for monitor is not a string");
println!("Loading monitor: {}", name);
.expect("Key `type` for monitor is not a string")
.to_owned();
let snd = mon_sender.clone();
let mut mon = plugins::monitor::factory(name, &mon_config);
thread::spawn(move || loop {
mon.run(&snd);
thread::spawn(move || {
log::info!("+> monitor: {}", mon_type);
match monitors::factory(&mon_type, &mon_config) {
Ok(mut mon) => loop {
mon.run(&snd);
},
Err(e) => log::error!("Cannot instantiate monitor {}: {}", mon_type, e),
}
});
}
// Instantiate output threads and structs
// let mut outputs: Vec<(Box<Output>, Filter)> = vec![];
let mut output_senders: Vec<mpsc::Sender<Message>> = vec![];
for out_config in config.outputs {
let name = out_config
.get("name")
.expect("Missing `name` key for monitor")
let out_type = out_config
.get("type")
.expect("Missing `type` key for output")
.as_str()
.expect("Key `name` for monitor is not a string");
println!("Loading output: {}", name);
.expect("Key `type` for output is not a string")
.to_owned();
let (out_sender, out_receiver) = mpsc::channel();
output_senders.push(out_sender);
let mut output = plugins::output::factory(name, &out_config);
let filter = Filter::new(&out_config);
thread::spawn(move || loop {
let message = out_receiver.recv().unwrap();
output.process_message(message);
thread::spawn(move || {
log::info!("+> output: {}", out_type);
match outputs::factory(&out_type, &out_config) {
Ok(mut output) => loop {
let message = out_receiver.recv().unwrap();
if filter.is_message_allowed(&message) {
output.process_message(message);
}
},
Err(e) => log::error!("Cannot instantiate output {}: {}", out_type, e),
}
});
}
let output_senders = output_senders;

View File

@ -1,10 +1,33 @@
#[derive(Debug, Clone)]
use serde::Deserialize;
pub use std::convert::TryFrom;
// pub type Level = u8;
#[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Hash, Deserialize)]
pub enum Level {
Debug = 0,
Info = 10,
Warning = 20,
Error = 30,
Critical = 40,
Notice = 1,
Anomaly = 2,
Issue = 3,
Critical = 4,
}
impl TryFrom<&str> for Level {
type Error = ();
fn try_from(level: &str) -> Result<Self, ()> {
match serde_yaml::from_str(level) {
Ok(v) => v,
_ => Err(()),
}
// match level {
// "Debug" => Ok(Level::Debug),
// "Notice" => Ok(Level::Notice),
// "Anomaly" => Ok(Level::Anomaly),
// "Issue" => Ok(Level::Issue),
// "Critical" => Ok(Level::Critical),
// _ => Err(()),
// }
}
}
#[derive(Debug, Clone)]

View File

@ -4,7 +4,7 @@ pub use std::collections::HashMap;
pub use std::sync::mpsc;
pub trait Monitor {
fn new(config: &HashMap<String, serde_yaml::Value>) -> Self
fn new(config: &HashMap<String, serde_yaml::Value>) -> Result<Self, Box<dyn std::error::Error>>
where
Self: Sized;
fn run(&mut self, sender: &mpsc::Sender<Message>);

View File

@ -1,6 +1,6 @@
pub use crate::message::*;
pub use crate::monitor::*;
use serde_derive::*;
use serde::Deserialize;
extern crate regex;
use regex::Regex;
@ -15,39 +15,49 @@ pub struct DHCPLeases {
rgx_date_start: Regex,
rgx_date_ends: Regex,
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[derive(Debug, PartialEq, Deserialize)]
struct DHCPLeasesConfig {
#[serde(default)]
path: String,
mac_rules: HashMap<String, Vec<String>>,
#[serde(default)]
mac_rules: HashMap<String, Option<Vec<String>>>,
}
impl Monitor for DHCPLeases {
fn new(config: &HashMap<String, serde_yaml::Value>) -> Self {
let config_node = config.get("config").expect("Missing `config` key").clone();
let config =
serde_yaml::from_value(config_node).expect("Invalid config for wifi_availability");
fn new(
config: &HashMap<String, serde_yaml::Value>,
) -> Result<Self, Box<dyn std::error::Error>> {
let config_node = config
.get("config")
.unwrap_or(&serde_yaml::Mapping::new().into())
.clone();
let config = serde_yaml::from_value(config_node).expect("Invalid config for dhcp_leases");
// Regex compilation
let rgx_lease = Regex::new(r"(?s)lease\s+(\d+(?:\.\d+){3})\s*\{\n?(.*?)\}").unwrap();
let rgx_mac =
Regex::new(r"^\s*hardware\s+ethernet\s([a-f0-9]{2}(?:\:[a-f0-9]{2}){5})\s*;").unwrap();
Regex::new(r"^\s*hardware\s+ethernet\s([a-f0-9]{2}(?::[a-f0-9]{2}){5})\s*;").unwrap();
let rgx_date_start = Regex::new(r"^\s*starts\s+(.*?)\s*;").unwrap();
let rgx_date_ends = Regex::new(r"^\s*ends\s+(.*?)\s*;").unwrap();
DHCPLeases {
Ok(DHCPLeases {
config: config,
rgx_lease: rgx_lease,
rgx_mac: rgx_mac,
rgx_date_start: rgx_date_start,
rgx_date_ends: rgx_date_ends,
}
})
}
fn run(&mut self, sender: &mpsc::Sender<Message>) {
use std::fs::File;
use std::io::prelude::*;
let config_file_path = match self.config.path.len() {
0 => "/var/lib/dhcp/dhcpd.leases",
_ => &self.config.path,
};
let mut config_file =
File::open(&self.config.path).expect("Could not open DHCP leases file");
File::open(config_file_path).expect("Could not open DHCP leases file");
let mut config_content = String::new();
config_file
.read_to_string(&mut config_content)
@ -91,19 +101,26 @@ impl Monitor for DHCPLeases {
if starts <= now && now < ends {
// Lease is active
if let Some(rules) = self.config.mac_rules.get(mac) {
for rule in rules {
if content.find(rule).is_none() {
unauthorized_macs.push(mac.to_owned());
// Found rules
if let Some(rules_list) = rules {
// Rules contains one or more entries
for rule in rules_list {
if content.find(rule).is_none() {
unauthorized_macs.push(mac.to_owned());
sender
.send(Message {
emitter: "dhcp_leases".to_owned(),
level: Level::Error,
msg_type: "dhcp_leases.unauthorized_mac.rule".to_owned(),
text: format!("Mismatching rule '{}' for device {}", rule, mac),
})
.unwrap();
break;
sender
.send(Message {
emitter: "dhcp_leases".to_owned(),
level: Level::Issue,
msg_type: "dhcp_leases.unauthorized_mac.rule".to_owned(),
text: format!(
"Mismatching rule '{}' for device {}",
rule, mac
),
})
.unwrap();
break;
}
}
}
} else {
@ -112,7 +129,7 @@ impl Monitor for DHCPLeases {
sender
.send(Message {
emitter: "dhcp_leases".to_owned(),
level: Level::Error,
level: Level::Issue,
msg_type: "dhcp_leases.unauthorized_mac.unknown".to_owned(),
text: format!("Unauthorized device on network: {}", mac),
})
@ -121,7 +138,19 @@ impl Monitor for DHCPLeases {
}
}
if unauthorized_macs.len() > 0 {}
if unauthorized_macs.len() > 0 {
sender
.send(Message {
emitter: "dhcp_leases".to_owned(),
level: Level::Issue,
msg_type: "dhcp_leases.unknown_mac".to_owned(),
text: format!(
"The following macs are not allowed: {:?}",
unauthorized_macs
),
})
.unwrap();
}
// let leases: Vec<(&str, &str)> = lease_rgx
// .captures_iter(&config_content)

View File

@ -0,0 +1,75 @@
pub use crate::message::{Level, Message};
pub use crate::monitor::*;
use serde::Deserialize;
#[derive(Debug)]
pub struct DummySender {
cnt: i64,
config: DummySenderConfig,
}
#[derive(Debug, Deserialize)]
struct DummySenderConfig {
#[serde(default)]
message: Option<String>,
#[serde(default)]
level: Option<Level>,
#[serde(default)]
msg_type: Option<String>,
#[serde(default)]
period: Option<f64>,
}
impl Monitor for DummySender {
fn new(
config: &HashMap<String, serde_yaml::Value>,
) -> Result<Self, Box<dyn std::error::Error>> {
let config_node = config
.get("config")
.unwrap_or(&serde_yaml::Mapping::new().into())
.clone();
let config: DummySenderConfig =
serde_yaml::from_value(config_node).expect("Invalid config for dummy_sender");
Ok(DummySender {
cnt: 0,
config: config,
})
}
fn run(&mut self, sender: &mpsc::Sender<Message>) {
log::info!(
"Sending message {} from {:?}",
self.cnt,
std::thread::current().id()
);
sender
.send(Message {
emitter: "dummy_sender".to_owned(),
level: match &self.config.level {
Some(l) => l.clone(),
None => Level::Notice,
},
msg_type: match &self.config.msg_type {
Some(s) => s.to_owned(),
None => "dummy_sender.message".to_owned(),
},
text: match &self.config.message {
Some(s) => s.to_owned(),
None => format!(
"This is message {} from {:?}",
self.cnt,
std::thread::current().id()
),
},
})
.unwrap();
self.cnt += 1;
let delay_ms = match self.config.period {
Some(sec) => (sec * 1000.0) as u64,
None => 2000,
};
std::thread::sleep(std::time::Duration::from_millis(delay_ms));
}
}

18
src/monitors/mod.rs Normal file
View File

@ -0,0 +1,18 @@
pub mod dhcp_leases;
pub mod dummy_sender;
pub mod wifi_availability;
use crate::monitor::*;
use std::collections::HashMap;
pub fn factory(
name: &str,
config: &HashMap<String, serde_yaml::Value>,
) -> Result<Box<Monitor>, Box<dyn std::error::Error>> {
match name {
"dummy_sender" => Ok(Box::new(dummy_sender::DummySender::new(&config)?)),
"wifi_availability" => Ok(Box::new(wifi_availability::WifiAvailability::new(&config)?)),
"dhcp_leases" => Ok(Box::new(dhcp_leases::DHCPLeases::new(&config)?)),
_ => panic!("Unknown monitor name: {}", name),
}
}

File diff suppressed because one or more lines are too long

View File

@ -1,22 +1,29 @@
use crate::message::Message;
use crate::message::{Level, Message};
use crate::monitor::*;
use serde_derive::*;
use serde::Deserialize;
#[derive(Debug)]
pub struct WifiAvailability {
config: WifiAvailabilityConfig,
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[derive(Debug, Deserialize)]
struct WifiAvailabilityConfig {
iface: String,
ssid: String,
#[serde(default)]
essid: Vec<String>,
#[serde(default)]
ping_targets: Vec<String>,
}
impl Monitor for WifiAvailability {
fn new(config: &HashMap<String, serde_yaml::Value>) -> Self {
let config_node = config.get("config").expect("Missing `config` key").clone();
fn new(
config: &HashMap<String, serde_yaml::Value>,
) -> Result<Self, Box<dyn std::error::Error>> {
let config_node = config
.get("config")
.unwrap_or(&serde_yaml::Mapping::new().into())
.clone();
let config =
serde_yaml::from_value(config_node).expect("Invalid config for wifi_availability");
@ -24,7 +31,7 @@ impl Monitor for WifiAvailability {
// Activate iface
//ip link set wlan0 up
wa
Ok(wa)
}
fn run(&mut self, sender: &mpsc::Sender<Message>) {
// iw dev wlan0 link: get is connected
@ -43,7 +50,7 @@ impl Monitor for WifiAvailability {
.send(Message {
emitter: "wifi_availability".to_owned(),
msg_type: "ping.failed".to_owned(),
level: 10,
level: Level::Issue,
text: format!("Cannot ping {}", target),
})
.unwrap();

View File

@ -1,52 +1,9 @@
extern crate globset;
pub use std::collections::HashMap;
use globset::Glob;
use std::iter::Map;
use crate::message::*;
#[derive(Debug)]
pub struct Filter {
level: Level,
types: Vec<Glob>,
}
impl Filter {
fn new(config: &HashMap<String, serde_yaml::Value>) -> Self {
let mut globs = vec![];
let types = config
.get("types")
.expect("Missing types key")
.as_sequence()
.expect("types must be a list of strings")
.iter()
.map(|x| {
Glob::new(x.as_str().expect("filter must be a list of strings"))
.expect("Invalid glob pattern")
.compile_matcher()
})
.collect();
// let glob = Glob::new("*.rs")?.compile_matcher();
Filter {
level: config.get("level").to.unwrap_or(Level::Debug),
types: types,
}
}
// fn is_message_filtered(&self, message: Message) -> bool {
// if message.level < self.level{
// return true
// }
// if
// }
}
pub trait Output {
fn new(config: &HashMap<String, serde_yaml::Value>) -> Self
fn new(config: &HashMap<String, serde_yaml::Value>) -> Result<Self, Box<dyn std::error::Error>>
where
Self: Sized;
fn process_message(&mut self, message: Message);

38
src/outputs/espeak.rs Normal file
View File

@ -0,0 +1,38 @@
use crate::message::Message;
pub use crate::output::*;
use serde::Deserialize;
#[derive(Debug)]
pub struct Espeak {
config: EspeakConfig,
}
#[derive(Debug, Deserialize)]
struct EspeakConfig {
#[serde(default = "espeak".to_owned())]
espeak: String,
#[serde(default = vec![])]
args: Vec<String>,
}
impl Output for Espeak {
fn new(
config: &HashMap<String, serde_yaml::Value>,
) -> Result<Self, Box<dyn std::error::Error>> {
let node = config
.get("config")
.unwrap_or(&serde_yaml::Value::Null)
.clone();
let cfg = serde_yaml::from_value(node).expect("Invalid config for Espeak");
Ok(Espeak { config: cfg })
}
fn process_message(&mut self, message: Message) {
use std::process::{Command, Stdio};
Command::new(&self.config.espeak)
.args(&self.config.args)
.arg(message.text)
.stdout(Stdio::null())
.status()
.expect("failed to execute process");
}
}

View File

@ -0,0 +1,298 @@
extern crate blurz;
use blurz::bluetooth_adapter::BluetoothAdapter;
use blurz::bluetooth_device::BluetoothDevice;
use blurz::bluetooth_gatt_characteristic::BluetoothGATTCharacteristic;
use blurz::bluetooth_gatt_service::BluetoothGATTService;
use blurz::bluetooth_session::BluetoothSession;
use globset::{Glob, GlobMatcher};
use serde::Deserialize;
use crate::message::{Level, Message};
pub use crate::output::*;
pub struct BluetoothLightbulb {
config: BluetoothLightbulbConfig,
session: BluetoothSession,
levels: HashMap<Level, LightConfig>,
msg_types: Vec<(GlobMatcher, LightConfig)>,
}
#[derive(Debug, Deserialize)]
struct BluetoothLightbulbConfig {
#[serde(default)]
mac: HashMap<String, ()>,
#[serde(default)]
levels: HashMap<Level, LightConfig>,
#[serde(default)]
msg_types: HashMap<String, LightConfig>,
}
#[derive(Debug, Clone, Deserialize)]
enum Animation {
None,
Smooth,
Bounce,
Blink,
RampUp,
RampDown,
}
#[derive(Debug, Clone, Deserialize)]
struct LightConfig {
#[serde(default)]
color: Option<u32>,
#[serde(default)]
animation: Option<Animation>,
#[serde(default)]
repeat: Option<u64>,
#[serde(default)]
speed: Option<f64>,
}
impl LightConfig {
fn merge(&mut self, other: &Self) {
if other.color.is_some() {
self.color = other.color;
}
if other.animation.is_some() {
self.animation = other.animation.clone();
}
if other.repeat.is_some() {
self.repeat = other.repeat;
}
if other.speed.is_some() {
self.speed = other.speed;
}
}
}
impl BluetoothLightbulb {
fn set_color(&self, characs: &Vec<BluetoothGATTCharacteristic>, r: f64, g: f64, b: f64) {
let r = (r * 255.0) as u8;
let g = (g * 255.0) as u8;
let b = (b * 255.0) as u8;
// println!("r={} g={} b={}", r, g, b);
for charac in characs {
charac
.write_value(vec![0x55, 0x13, r, g, b, '\r' as u8, '\n' as u8], None)
.unwrap();
}
}
fn initiate_scan(&self, adapter: &BluetoothAdapter) {
if !adapter.is_discovering().unwrap() {
log::info!("bluetooth adapter {}: Set scan ON", adapter.get_id());
adapter
.start_discovery()
.expect("Cannot activate scanning on bluetooth device");
}
}
}
impl Output for BluetoothLightbulb {
fn new(
config: &HashMap<String, serde_yaml::Value>,
) -> Result<Self, Box<dyn std::error::Error>> {
let config_node = config
.get("config")
.unwrap_or(&serde_yaml::Mapping::new().into())
.clone();
let config: BluetoothLightbulbConfig =
serde_yaml::from_value(config_node).expect("Invalid config for light_beewi_bbl227");
let msg_types = config
.msg_types
.iter()
.map(|(k, v)| {
(
Glob::new(k)
.expect("Invalid glob pattern")
.compile_matcher(),
v.clone(),
)
})
.collect();
let levels = config
.levels
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
let session =
BluetoothSession::create_session(None).expect("Could not initialize bluetooth session");
let ret = BluetoothLightbulb {
config: config,
msg_types: msg_types,
levels: levels,
session: session,
};
// Initialize bluetooth device
let adapter =
BluetoothAdapter::init(&ret.session).expect("Could not initialize bluetooth adapter");
// Power on
if !adapter.is_powered().unwrap() {
log::info!("bluetooth adapter {}: Set power ON", adapter.get_id());
adapter
.set_powered(true)
.expect("Cannot power on bluetooth device");
}
// Start scan
ret.initiate_scan(&adapter);
Ok(ret)
}
fn process_message(&mut self, message: Message) {
let adapter =
BluetoothAdapter::init(&self.session).expect("Could not initialize bluetooth adapter");
let mut characs = vec![];
let mut found_macs: HashMap<String, bool> = HashMap::new();
for mac in self.config.mac.keys() {
found_macs.insert(mac.to_owned(), false);
}
for dev_path in adapter.get_device_list().unwrap() {
let device = BluetoothDevice::new(&self.session, dev_path);
let addr = device.get_address().unwrap();
if self.config.mac.contains_key(&addr) {
if !device.is_connected().unwrap() {
log::info!("Connecting to {}", addr);
match device.connect(5000) {
Ok(_) => {
let power = device.get_tx_power().expect("Could not request tx power");
log::info!("Connected to {}, tx power={}", addr, power);
}
Err(e) => {
log::error!("Could not connect to {}: {}", addr, e);
continue;
}
}
}
for srv_path in device.get_gatt_services().unwrap() {
let service = BluetoothGATTService::new(&self.session, srv_path);
let service_uuid = service.get_uuid().unwrap();
if service_uuid == "a8b3fff0-4834-4051-89d0-3de95cddd318" {
for charac_path in service.get_gatt_characteristics().unwrap() {
let charac = BluetoothGATTCharacteristic::new(
&self.session,
charac_path.clone(),
);
let charac_uuid = charac.get_uuid().unwrap();
if charac_uuid == "a8b3fff1-4834-4051-89d0-3de95cddd318" {
characs.push(charac);
found_macs.insert(addr.clone(), true);
}
}
}
}
}
}
let macs_not_found: HashMap<String, bool> = found_macs
.into_iter()
.filter(|(_, v)| *v == false)
.collect();
if macs_not_found.len() > 0 {
log::warn!(
"Could not find configured lightbulbs: {:?}",
macs_not_found.keys()
);
self.initiate_scan(&adapter);
return;
}
// find out color & pattern to play
let mut cfg = LightConfig {
color: Some(0xffffff),
animation: Some(Animation::Smooth),
repeat: Some(1),
speed: Some(1.0),
};
for (type_glob, type_cfg) in &self.msg_types {
if type_glob.is_match(&message.msg_type) {
cfg.merge(type_cfg);
break;
}
}
if let Some(level_cfg) = self.levels.get(&message.level) {
cfg.merge(level_cfg);
}
let color = cfg.color.unwrap();
let r = ((color >> 16) & 0xff) as f64 / 255.0;
let g = ((color >> 8) & 0xff) as f64 / 255.0;
let b = ((color >> 0) & 0xff) as f64 / 255.0;
let anim = cfg.animation.unwrap();
let speed = cfg.speed.unwrap();
// Play animation
log::debug!("Playing {:?} {:?}", anim, color);
for _ in 0..cfg.repeat.unwrap() {
match anim {
Animation::None => {
self.set_color(&characs, r, g, b);
std::thread::sleep(std::time::Duration::from_millis((2000.0 / speed) as u64));
}
Animation::Bounce => {
for _ in 0..4 {
self.set_color(&characs, r, g, b);
std::thread::sleep(std::time::Duration::from_millis(
(250.0 / speed) as u64,
));
self.set_color(&characs, r * 0.5, g * 0.5, b * 0.5);
std::thread::sleep(std::time::Duration::from_millis(
(250.0 / speed) as u64,
));
}
}
Animation::Smooth => {
for i in 0..10 {
let mult = i as f64 / 10.0;
self.set_color(&characs, r * mult, g * mult, b * mult);
}
for i in 0..10 {
let mult = 1.0 - (i as f64 / 10.0);
self.set_color(&characs, r * mult, g * mult, b * mult);
}
}
Animation::Blink => {
for i in 0..4 {
let (r, g, b) = match i % 2 {
0 => (r, g, b),
_ => (0.0, 0.0, 0.0),
};
self.set_color(&characs, r, g, b);
std::thread::sleep(std::time::Duration::from_millis(
(400.0 / speed) as u64,
));
}
}
Animation::RampUp => {
for i in 0..20 {
let mult = i as f64 / 20.0;
self.set_color(&characs, r * mult, g * mult, b * mult);
}
std::thread::sleep(std::time::Duration::from_millis(100));
self.set_color(&characs, 0.0, 0.0, 0.0);
std::thread::sleep(std::time::Duration::from_millis(100));
}
Animation::RampDown => {
self.set_color(&characs, r, g, b);
std::thread::sleep(std::time::Duration::from_millis(100));
for i in 1..20 {
let mult = 1.0 - (i as f64 / 20.0);
self.set_color(&characs, r * mult, g * mult, b * mult);
}
self.set_color(&characs, 0.0, 0.0, 0.0);
std::thread::sleep(std::time::Duration::from_millis(100));
}
}
}
self.set_color(&characs, 0.0, 0.0, 0.0);
}
}

20
src/outputs/mod.rs Normal file
View File

@ -0,0 +1,20 @@
pub mod espeak;
pub mod light_beewi_bbl227;
pub mod stdout;
use crate::output::*;
use std::collections::HashMap;
pub fn factory(
name: &str,
config: &HashMap<String, serde_yaml::Value>,
) -> Result<Box<Output>, Box<dyn std::error::Error>> {
match name {
"stdout" => Ok(Box::new(stdout::Stdout::new(&config)?)),
"espeak" => Ok(Box::new(espeak::Espeak::new(&config)?)),
"light_beewi_bbl227" => Ok(Box::new(light_beewi_bbl227::BluetoothLightbulb::new(
&config,
)?)),
_ => panic!("Unknown monitor name: {}", name),
}
}

27
src/outputs/stdout.rs Normal file
View File

@ -0,0 +1,27 @@
use crate::message::*;
pub use crate::output::*;
#[derive(Debug)]
pub struct Stdout {}
impl Output for Stdout {
fn new(
_config: &HashMap<String, serde_yaml::Value>,
) -> Result<Self, Box<dyn std::error::Error>> {
Ok(Stdout {})
}
fn process_message(&mut self, message: Message) {
log::info!(
"{}{}: {}\x1B[m",
match message.level {
Level::Debug => "\x1B[2m",
Level::Notice => "",
Level::Anomaly => "\x1B[33m",
Level::Issue => "\x1B[31m",
Level::Critical => "\x1B[31;40;1m",
},
message.msg_type,
message.text
);
}
}

View File

@ -1,2 +0,0 @@
pub mod monitor;
pub mod output;

View File

@ -1,15 +0,0 @@
pub mod dhcp_leases;
pub mod tester;
pub mod wifi_availability;
use crate::monitor::*;
use std::collections::HashMap;
pub fn factory(name: &str, config: &HashMap<String, serde_yaml::Value>) -> Box<Monitor + Send> {
match name {
"tester" => Box::new(tester::Tester::new(&config)),
"wifi_availability" => Box::new(wifi_availability::WifiAvailability::new(&config)),
"dhcp_leases" => Box::new(dhcp_leases::DHCPLeases::new(&config)),
_ => panic!("Unknown monitor name: {}", name),
}
}

View File

@ -1,27 +0,0 @@
pub use crate::message::Message;
pub use crate::monitor::*;
#[derive(Debug)]
pub struct Tester {
cnt: i64,
}
impl Monitor for Tester {
fn new(config: &HashMap<String, serde_yaml::Value>) -> Self {
Tester { cnt: 0 }
}
fn run(&mut self, sender: &mpsc::Sender<Message>) {
println!("-- Sending message {}", self.cnt);
sender
.send(Message {
emitter: "tester".to_string(),
level: 10,
msg_type: "string".to_string(),
text: format!("This is message number {}", self.cnt),
})
.unwrap();
self.cnt += 1;
std::thread::sleep(std::time::Duration::from_millis(2000));
}
}

View File

@ -1,37 +0,0 @@
extern crate blurz;
// use blurz::bluetooth_adapter::BluetoothAdapter;
// use blurz::bluetooth_device::BluetoothDevice;
// use blurz::bluetooth_gatt_characteristic::BluetoothGATTCharacteristic;
// use blurz::bluetooth_gatt_service::BluetoothGATTService;
// use crate::message::Message;
// pub use crate::output::*;
// use serde_derive::*;
// #[derive(Debug)]
// pub struct BluetoothLightbulb<'a> {
// config: BluetoothLightbulbConfig,
// adapter: BluetoothAdapter<'a>,
// }
// #[derive(Debug, PartialEq, Serialize, Deserialize)]
// struct BluetoothLightbulbConfig {
// mac: Vec<String>,
// }
// impl Output for BluetoothLightbulb {
// fn new(config: &HashMap<String, serde_yaml::Value>) -> Self {
// let config_node = config.get("config").expect("Missing `config` key").clone();
// let config =
// serde_yaml::from_value(config_node).expect("Invalid config for wifi_availability");
// let adapter = BluetoothAdapter::init().expect("Cannot initialize bluetooth adapter");
// BluetoothLightbulb { config: config }
// }
// fn process_message(&mut self, message: Message) {
// println!("Received message: {:?}", message);
// let device = Device::new(dev_path);
// }
// }

View File

@ -1,19 +0,0 @@
use crate::message::Message;
pub use crate::output::*;
#[derive(Debug)]
pub struct Espeak {}
impl Output for Espeak {
fn new(config: &HashMap<String, serde_yaml::Value>) -> Self {
Espeak {}
}
fn process_message(&mut self, message: Message) {
use std::process::{Command, Stdio};
let res = Command::new("espeak")
.arg(message.text)
.stdout(Stdio::null())
.status()
.expect("failed to execute process");
}
}

View File

@ -1,14 +0,0 @@
pub mod bluetooth_lightbulb;
pub mod espeak;
pub mod stdout;
use crate::output::*;
use std::collections::HashMap;
pub fn factory(name: &str, config: &HashMap<String, serde_yaml::Value>) -> Box<Output + Send> {
match name {
"stdout" => Box::new(stdout::Stdout::new(&config)),
"espeak" => Box::new(espeak::Espeak::new(&config)),
_ => panic!("Unknown monitor name: {}", name),
}
}

View File

@ -1,14 +0,0 @@
use crate::message::Message;
pub use crate::output::*;
#[derive(Debug)]
pub struct Stdout {}
impl Output for Stdout {
fn new(config: &HashMap<String, serde_yaml::Value>) -> Self {
Stdout {}
}
fn process_message(&mut self, message: Message) {
println!("Received message: {:?}", message);
}
}