Architecture rework and matrix output

This commit is contained in:
Crom (Thibaut CHARLES) 2019-02-23 18:45:43 +01:00
parent d8ebb16486
commit 2df8a9351f
Signed by: tcharles
GPG Key ID: 45A3D5F880B9E6D0
19 changed files with 1626 additions and 216 deletions

2
.gitignore vendored
View File

@ -1,2 +1,4 @@
/target /target
**/*.rs.bk **/*.rs.bk
config.yaml

1140
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -8,6 +8,7 @@ edition = "2018"
libloading = "0.5.0" libloading = "0.5.0"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_yaml = "0.8" serde_yaml = "0.8"
serde_json = "1.0"
getopts = "0.2" getopts = "0.2"
blurz = "0.4.0" blurz = "0.4.0"
regex = "1" regex = "1"
@ -15,3 +16,6 @@ chrono = "0.4"
globset = "0.3" globset = "0.3"
log = "0.4" log = "0.4"
simplelog = "0.5" simplelog = "0.5"
reqwest = "0.9"
percent-encoding = "1.0"
rand = "0.6"

67
config.example.yaml Normal file
View File

@ -0,0 +1,67 @@
monitors:
# - type: wifi_availability
# config:
# iface: lo
# ssid: ""
# essid: []
# ping_targets: ["127.0.0.1"]
# - type: dummy_sender
# config:
# level: Issue
# period: 15.0
- type: dummy_sender
config:
level: Critical
period: 10.0
# - type: dummy_sender
# config:
# level: Anomaly
# period: 15.0
- type: dhcp_leases
config:
hello: world
path: /home/crom/GitProjects/rnetmon/leases.example.txt
mac_rules:
d4:c9:ef:50:aa:c8: # laptop eth0
- 'binding state free'
- 'uid "\001@\210\005\355`\205"'
- 'set vendor-class-identifier = "android-dhcp-7.1.2"'
# 3c:a9:f4:4c:a3:38: # laptop wlan0
a0:1b:29:7f:37:9e:
- uid "\001\240\033)\1777\236";
- set vendor-class-identifier = "udhcp 1.20.2";
outputs:
- type: stdout
# - type: espeak
# - type: light_beewi_bbl227
# filter:
# include_types: ["*"]
# config:
# mac:
# # ? "84:EB:18:7C:22:68" # Bedroom lightbulb
# ? "84:EB:18:7C:1C:F7" # Testing lightbulb
# msg_types:
# "*":
# animation: Smooth
# repeat: 2
# speed: 1.5
# color: 0x808080
# # wifi.intrusion.*:
# # color: 0xff0000
# # pattern: blink
# # duration: 5.0
# # speed: 1.0
# # ping.*:
# # color: 0xff8000
# levels:
# Critical:
# animation: Blink
# color: 0xFF0000
# Anomaly:
# animation: Bounce
# color: 0x00FF00

View File

@ -1,22 +0,0 @@
monitors:
# - name: wifi_availability
# config:
# iface: lo
# ssid: ""
# essid: []
# ping_targets: ["127.0.0.1"]
# - name: tester
- name: dhcp_leases
config:
path: /home/crom/GitProjects/rnetmon/leases.example.txt
mac_rules:
d4:c9:ef:50:aa:c8: # laptop eth0
- binding state free
- uid "\001@\210\005\355`\205"
- set vendor-class-identifier = "android-dhcp-7.1.2"
3c:a9:f4:4c:a3:38 # laptop wlan0
outputs:
# - name: stdout
- name: espeak

View File

@ -2,8 +2,6 @@ use globset::{Glob, GlobMatcher};
pub use std::collections::HashMap; pub use std::collections::HashMap;
use log::{info, trace, warn};
use crate::message::*; use crate::message::*;
#[derive(Debug)] #[derive(Debug)]
@ -14,13 +12,8 @@ pub struct Filter {
} }
impl Filter { impl Filter {
pub fn new(config: &HashMap<String, serde_yaml::Value>) -> Self { pub fn new(config: &serde_yaml::Value) -> Self {
let filter_node = config let include_types = match config.get("include_types") {
.get("filter")
.unwrap_or(&serde_yaml::Mapping::new().into())
.clone();
let include_types = match filter_node.get("include_types") {
Some(v) => v Some(v) => v
.as_sequence() .as_sequence()
.expect("include_types must be a list of strings") .expect("include_types must be a list of strings")
@ -33,7 +26,7 @@ impl Filter {
.collect(), .collect(),
None => vec![], None => vec![],
}; };
let exclude_types = match filter_node.get("exclude_types") { let exclude_types = match config.get("exclude_types") {
Some(v) => v Some(v) => v
.as_sequence() .as_sequence()
.expect("exclude_types must be a list of strings") .expect("exclude_types must be a list of strings")
@ -47,7 +40,7 @@ impl Filter {
None => vec![], None => vec![],
}; };
let level = match filter_node.get("level") { let level = match config.get("level") {
Some(l) => Level::try_from(l.as_str().expect("Level must be a string")) Some(l) => Level::try_from(l.as_str().expect("Level must be a string"))
.expect("Unknown level name"), .expect("Unknown level name"),
None => Level::Notice, None => Level::Notice,

View File

@ -102,24 +102,30 @@ fn main() {
// Instantiate monitor threads and structs // Instantiate monitor threads and structs
let mut mon_threads = vec![]; let mut mon_threads = vec![];
for mon_config in config.monitors { for mon_node in config.monitors {
let mon_type = mon_config let mon_type = mon_node
.get("type") .get("type")
.expect("Missing `type` key for monitor") .expect("Missing `type` key for monitor")
.as_str() .as_str()
.expect("Key `type` for monitor is not a string") .expect("Key `type` for monitor is not a string")
.to_owned(); .to_owned();
let mon_config = mon_node
.get("config")
.unwrap_or(&serde_yaml::Mapping::new().into())
.clone();
let snd = mon_sender.clone(); let snd = mon_sender.clone();
let bar = barrier.clone(); let bar = barrier.clone();
mon_threads.push(thread::spawn(move || { mon_threads.push(thread::spawn(move || {
log::info!("+> monitor: {}", mon_type); log::info!("+> monitor: {}", mon_type);
match monitors::factory(&mon_type, &mon_config) { match monitors::factory(&mon_type, mon_config) {
Ok(mut mon) => loop { Ok(mut mon) => {
bar.wait(); bar.wait();
mon.run(&snd); loop {
}, mon.run(&snd);
}
}
Err(e) => log::error!("Cannot instantiate monitor {}: {}", mon_type, e), Err(e) => log::error!("Cannot instantiate monitor {}: {}", mon_type, e),
} }
})); }));
@ -128,30 +134,40 @@ fn main() {
// Instantiate output threads and structs // Instantiate output threads and structs
let mut output_senders: Vec<mpsc::Sender<Message>> = vec![]; let mut output_senders: Vec<mpsc::Sender<Message>> = vec![];
let mut output_threads = vec![]; let mut output_threads = vec![];
for out_config in config.outputs { for out_node in config.outputs {
let out_type = out_config let out_type = out_node
.get("type") .get("type")
.expect("Missing `type` key for output") .expect("Missing `type` key for output")
.as_str() .as_str()
.expect("Key `type` for output is not a string") .expect("Key `type` for output is not a string")
.to_owned(); .to_owned();
let out_config = out_node
.get("config")
.unwrap_or(&serde_yaml::Mapping::new().into())
.clone();
let out_filter = out_node
.get("filter")
.unwrap_or(&serde_yaml::Mapping::new().into())
.clone();
let (out_sender, out_receiver) = mpsc::channel(); let (out_sender, out_receiver) = mpsc::channel();
output_senders.push(out_sender); output_senders.push(out_sender);
let filter = Filter::new(&out_config); let filter = Filter::new(&out_filter);
let bar = barrier.clone(); let bar = barrier.clone();
output_threads.push(thread::spawn(move || { output_threads.push(thread::spawn(move || {
log::info!("+> output: {}", out_type); log::info!("+> output: {}", out_type);
match outputs::factory(&out_type, &out_config) { match outputs::factory(&out_type, out_config) {
Ok(mut output) => loop { Ok(mut output) => {
bar.wait(); bar.wait();
let message = out_receiver.recv().unwrap(); loop {
if filter.is_message_allowed(&message) { let message = out_receiver.recv().unwrap();
output.process_message(message); if filter.is_message_allowed(&message) {
output.process_message(message);
}
} }
}, }
Err(e) => log::error!("Cannot instantiate output {}: {}", out_type, e), Err(e) => log::error!("Cannot instantiate output {}: {}", out_type, e),
} }
})); }));

View File

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

View File

@ -5,7 +5,7 @@ use serde::Deserialize;
extern crate regex; extern crate regex;
use regex::Regex; use regex::Regex;
extern crate chrono; extern crate chrono;
use chrono::{DateTime, Local}; use chrono::Local;
#[derive(Debug)] #[derive(Debug)]
pub struct DHCPLeases { pub struct DHCPLeases {
@ -21,17 +21,13 @@ struct DHCPLeasesConfig {
path: String, path: String,
#[serde(default)] #[serde(default)]
mac_rules: HashMap<String, Option<Vec<String>>>, mac_rules: HashMap<String, Option<Vec<String>>>,
#[serde(default)]
period: Option<f64>,
} }
impl Monitor for DHCPLeases { impl Monitor for DHCPLeases {
fn new( fn new(config: serde_yaml::Value) -> Result<Self, Box<dyn std::error::Error>> {
config: &HashMap<String, serde_yaml::Value>, let config = serde_yaml::from_value(config)?;
) -> 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)?;
// Regex compilation // Regex compilation
let rgx_lease = Regex::new(r"(?s)lease\s+(\d+(?:\.\d+){3})\s*\{\n?(.*?)\}").unwrap(); let rgx_lease = Regex::new(r"(?s)lease\s+(\d+(?:\.\d+){3})\s*\{\n?(.*?)\}").unwrap();
@ -67,77 +63,76 @@ impl Monitor for DHCPLeases {
let mut unauthorized_macs: Vec<String> = vec![]; let mut unauthorized_macs: Vec<String> = vec![];
for cap in self.rgx_lease.captures_iter(&config_content) { for cap in self.rgx_lease.captures_iter(&config_content) {
let _ip = cap.get(1).unwrap().as_str(); let ip = cap.get(1).unwrap().as_str();
let content = cap.get(2).unwrap().as_str(); let content = cap.get(2).unwrap().as_str();
let mac = self if let Some(mac_cap) = self.rgx_mac.captures(content) {
.rgx_mac let mac = mac_cap.get(1).unwrap().as_str();
.captures(content)
.expect(&format!(
"No 'hardware ethernet' field found for MAC address in {}",
content
))
.get(1)
.unwrap()
.as_str();
let starts_str = self
.rgx_date_start
.captures(content)
.expect("No 'starts' field found in lease")
.get(1)
.unwrap()
.as_str();
let starts =
chrono::naive::NaiveDateTime::parse_from_str(starts_str, "%Y/%m/%d %H:%M:%S")
.expect(&format!("Bad date format: '{}'", starts_str));
let ends_str = self
.rgx_date_ends
.captures(content)
.expect("No 'ends' field found in lease")
.get(1)
.unwrap()
.as_str();
let ends = chrono::naive::NaiveDateTime::parse_from_str(ends_str, "%Y/%m/%d %H:%M:%S")
.expect(&format!("Bad date format: '{}'", ends_str));
let now = Local::now().naive_local(); let starts_str = self
if starts <= now && now < ends { .rgx_date_start
// Lease is active .captures(content)
if let Some(rules) = self.config.mac_rules.get(mac) { .expect("No 'starts' field found in lease")
// Found rules .get(1)
if let Some(rules_list) = rules { .unwrap()
// Rules contains one or more entries .as_str();
for rule in rules_list { let starts =
if content.find(rule).is_none() { chrono::naive::NaiveDateTime::parse_from_str(starts_str, "%Y/%m/%d %H:%M:%S")
unauthorized_macs.push(mac.to_owned()); .expect(&format!("Bad date format: '{}'", starts_str));
let ends_str = self
.rgx_date_ends
.captures(content)
.expect("No 'ends' field found in lease")
.get(1)
.unwrap()
.as_str();
let ends =
chrono::naive::NaiveDateTime::parse_from_str(ends_str, "%Y/%m/%d %H:%M:%S")
.expect(&format!("Bad date format: '{}'", ends_str));
sender let now = Local::now().naive_utc();
.send(Message { if starts <= now && now < ends {
emitter: "dhcp_leases".to_owned(), log::debug!("Found an active DHCP lease for {}", ip);
level: Level::Issue, // Lease is active
msg_type: "dhcp_leases.unauthorized_mac.rule".to_owned(), if let Some(rules) = self.config.mac_rules.get(mac) {
text: format!( // Found rules
"Mismatching rule '{}' for device {}", if let Some(rules_list) = rules {
rule, mac // Rules contains one or more entries
), for rule in rules_list {
}) if content.find(rule).is_none() {
.unwrap(); unauthorized_macs.push(mac.to_owned());
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 {} at IP {}",
rule, mac, ip
),
})
.unwrap();
break;
}
} }
} }
} } else {
} else { unauthorized_macs.push(mac.to_owned());
unauthorized_macs.push(mac.to_owned());
sender sender
.send(Message { .send(Message {
emitter: "dhcp_leases".to_owned(), emitter: "dhcp_leases".to_owned(),
level: Level::Issue, level: Level::Issue,
msg_type: "dhcp_leases.unauthorized_mac.unknown".to_owned(), msg_type: "dhcp_leases.unauthorized_mac.unknown".to_owned(),
text: format!("Unauthorized device on network: {}", mac), text: format!("Unknown device {} using IP {}", mac, ip),
}) })
.unwrap(); .unwrap();
}
} }
} else {
log::warn!("No 'hardware ethernet' field found for IP {}", ip);
} }
} }
@ -146,7 +141,7 @@ impl Monitor for DHCPLeases {
.send(Message { .send(Message {
emitter: "dhcp_leases".to_owned(), emitter: "dhcp_leases".to_owned(),
level: Level::Issue, level: Level::Issue,
msg_type: "dhcp_leases.unknown_mac".to_owned(), msg_type: "dhcp_leases.unknown_mac.sumup".to_owned(),
text: format!( text: format!(
"The following macs are not allowed: {:?}", "The following macs are not allowed: {:?}",
unauthorized_macs unauthorized_macs
@ -155,23 +150,9 @@ impl Monitor for DHCPLeases {
.unwrap(); .unwrap();
} }
// let leases: Vec<(&str, &str)> = lease_rgx std::thread::sleep(std::time::Duration::from_millis(
// .captures_iter(&config_content) (self.config.period.unwrap_or(60.0) * 1000.0) as u64,
// .map(|c| (c.get(1).unwrap().as_str(), c.get(2).unwrap().as_str())) ));
// .collect();
// println!("{:?}", leases);
// let cap = lease_rgx.captures(config_content);
// sender
// .send(Message {
// emitter: "dhcp_leases".to_owned(),
// level: 10,
// msg_type: "string".to_owned(),
// text: format!("frfr"),
// })
// .unwrap();
std::thread::sleep(std::time::Duration::from_millis(2000));
} }
} }

View File

@ -21,15 +21,9 @@ struct DummySenderConfig {
} }
impl Monitor for DummySender { impl Monitor for DummySender {
fn new( fn new(config: serde_yaml::Value) -> Result<Self, Box<dyn std::error::Error>> {
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 = let config: DummySenderConfig =
serde_yaml::from_value(config_node).expect("Invalid config for dummy_sender"); serde_yaml::from_value(config).expect("Invalid config for dummy_sender");
Ok(DummySender { Ok(DummySender {
cnt: 0, cnt: 0,

View File

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

View File

@ -0,0 +1,180 @@
pub use crate::message::*;
pub use crate::monitor::*;
use serde::Deserialize;
extern crate regex;
use regex::Regex;
extern crate chrono;
use chrono::Local;
#[derive(Debug)]
pub struct NmapScanner {
config: NmapScannerConfig,
rgx_lease: Regex,
rgx_mac: Regex,
rgx_date_start: Regex,
rgx_date_ends: Regex,
}
#[derive(Debug, PartialEq, Deserialize)]
struct NmapScannerConfig {
#[serde(default)]
args: Vec<String>,
#[serde(default)]
ip_range: Option<String>,
}
impl Monitor for NmapScanner {
fn new(
config: serde_yaml::Value,
) -> Result<Self, Box<dyn std::error::Error>> {
let mut config = serde_yaml::from_value(config)?;
if config.ip_range.is_none() {
// Detect current subnet
config.ip_range = Some("192.168.0.1-254");
}
// Regex compilation
let rgx_lease = Regex::new(r"(?s)lease\s+(\d+(?:\.\d+){3})\s*\{\n?(.*?)\}").unwrap();
let rgx_mac =
Regex::new(r"(?m)^\s*hardware\s+ethernet\s([a-f0-9]{2}(?::[a-f0-9]{2}){5})\s*;")
.unwrap();
let rgx_date_start = Regex::new(r"(?m)^\s*starts\s+\d+\s+(.*?)\s*;").unwrap();
let rgx_date_ends = Regex::new(r"(?m)^\s*ends\s+\d+\s+(.*?)\s*;").unwrap();
Ok(NmapScanner {
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>) {
nmap -sV -T4 -O -F --version-light 192.168.0.1-254
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(config_file_path).expect("Could not open DHCP leases file");
let mut config_content = String::new();
config_file
.read_to_string(&mut config_content)
.expect("Could not read DHCP leases file");
let mut unauthorized_macs: Vec<String> = vec![];
for cap in self.rgx_lease.captures_iter(&config_content) {
let ip = cap.get(1).unwrap().as_str();
let content = cap.get(2).unwrap().as_str();
if let Some(mac_cap) = self.rgx_mac.captures(content) {
let mac = mac_cap.get(1).unwrap().as_str();
let starts_str = self
.rgx_date_start
.captures(content)
.expect("No 'starts' field found in lease")
.get(1)
.unwrap()
.as_str();
let starts =
chrono::naive::NaiveDateTime::parse_from_str(starts_str, "%Y/%m/%d %H:%M:%S")
.expect(&format!("Bad date format: '{}'", starts_str));
let ends_str = self
.rgx_date_ends
.captures(content)
.expect("No 'ends' field found in lease")
.get(1)
.unwrap()
.as_str();
let ends =
chrono::naive::NaiveDateTime::parse_from_str(ends_str, "%Y/%m/%d %H:%M:%S")
.expect(&format!("Bad date format: '{}'", ends_str));
let now = Local::now().naive_utc();
if starts <= now && now < ends {
log::debug!("Found an active DHCP lease for {}", ip);
// Lease is active
if let Some(rules) = self.config.mac_rules.get(mac) {
// 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::Issue,
msg_type: "dhcp_leases.unauthorized_mac.rule"
.to_owned(),
text: format!(
"Mismatching rule '{}' for device {} at IP {}",
rule, mac, ip
),
})
.unwrap();
break;
}
}
}
} else {
unauthorized_macs.push(mac.to_owned());
sender
.send(Message {
emitter: "dhcp_leases".to_owned(),
level: Level::Issue,
msg_type: "dhcp_leases.unauthorized_mac.unknown".to_owned(),
text: format!("Unknown device {} using IP {}", mac, ip),
})
.unwrap();
}
}
} else {
log::warn!("No 'hardware ethernet' field found for IP {}", ip);
}
}
if unauthorized_macs.len() > 0 {
sender
.send(Message {
emitter: "dhcp_leases".to_owned(),
level: Level::Issue,
msg_type: "dhcp_leases.unknown_mac.sumup".to_owned(),
text: format!(
"The following macs are not allowed: {:?}",
unauthorized_macs
),
})
.unwrap();
}
std::thread::sleep(std::time::Duration::from_millis(
(self.config.period.unwrap_or(60.0) * 1000.0) as u64,
));
}
}
/*
lease 192.168.0.26 {
starts 4 2018/08/16 22:31:22;
ends 4 2018/08/16 23:01:22;
tstp 4 2018/08/16 23:01:22;
cltt 4 2018/08/16 22:31:22;
binding state free;
hardware ethernet 84:4b:f5:16:f4:94;
}
*/

View File

@ -17,15 +17,8 @@ struct WifiAvailabilityConfig {
} }
impl Monitor for WifiAvailability { impl Monitor for WifiAvailability {
fn new( fn new(config: serde_yaml::Value) -> Result<Self, Box<dyn std::error::Error>> {
config: &HashMap<String, serde_yaml::Value>, let config = serde_yaml::from_value(config).expect("Invalid config for wifi_availability");
) -> 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");
let wa = WifiAvailability { config: config }; let wa = WifiAvailability { config: config };
// Activate iface // Activate iface

View File

@ -3,7 +3,7 @@ pub use std::collections::HashMap;
use crate::message::*; use crate::message::*;
pub trait Output { pub trait Output {
fn new(config: &HashMap<String, serde_yaml::Value>) -> Result<Self, Box<dyn std::error::Error>> fn new(config: serde_yaml::Value) -> Result<Self, Box<dyn std::error::Error>>
where where
Self: Sized; Self: Sized;
fn process_message(&mut self, message: Message); fn process_message(&mut self, message: Message);

View File

@ -15,14 +15,8 @@ struct EspeakConfig {
} }
impl Output for Espeak { impl Output for Espeak {
fn new( fn new(config: serde_yaml::Value) -> Result<Self, Box<dyn std::error::Error>> {
config: &HashMap<String, serde_yaml::Value>, let cfg = serde_yaml::from_value(config).expect("Invalid config for Espeak");
) -> 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 }) Ok(Espeak { config: cfg })
} }

View File

@ -99,14 +99,8 @@ impl BluetoothLightbulb {
} }
impl Output for BluetoothLightbulb { impl Output for BluetoothLightbulb {
fn new( fn new(config: serde_yaml::Value) -> Result<Self, Box<dyn std::error::Error>> {
config: &HashMap<String, serde_yaml::Value>, let config: BluetoothLightbulbConfig = serde_yaml::from_value(config)?;
) -> 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)?;
let msg_types = config let msg_types = config
.msg_types .msg_types
@ -244,7 +238,7 @@ impl Output for BluetoothLightbulb {
let speed = cfg.speed.unwrap(); let speed = cfg.speed.unwrap();
// Play animation // Play animation
log::debug!("Playing {:?} {:?}", anim, color); log::debug!("Playing {:?} {:X}", anim, color);
for _ in 0..cfg.repeat.unwrap() { for _ in 0..cfg.repeat.unwrap() {
match anim { match anim {

132
src/outputs/matrix.rs Normal file
View File

@ -0,0 +1,132 @@
extern crate rand;
use crate::message::*;
pub use crate::output::*;
use serde::Deserialize;
use percent_encoding::{utf8_percent_encode, DEFAULT_ENCODE_SET};
use rand::distributions::Alphanumeric;
use rand::Rng;
use reqwest::header;
use reqwest::{Client, Url};
#[derive(Debug)]
struct MatrixClient {
http_client: Client,
homeserver_uri: String,
token: String,
}
impl MatrixClient {
fn new(homeserver_uri: &str, token: &str) -> Self {
Self {
http_client: Client::new(),
homeserver_uri: homeserver_uri.to_owned(),
token: token.to_owned(),
}
}
// fn get(
// &self,
// path: &str,
// params: &HashMap<&str, String>,
// ) -> Result<reqwest::Response, reqwest::Error> {
// let mut path = self.homeserver_uri.clone() + path;
// let mut first = true;
// for (k, v) in params {
// path += &format!(
// "{}{}={}",
// if first { "?" } else { "&" },
// utf8_percent_encode(k.as_ref(), DEFAULT_ENCODE_SET),
// utf8_percent_encode(v.as_ref(), DEFAULT_ENCODE_SET)
// );
// first = false;
// }
// self.http_client
// .get(Url::parse(&path).unwrap())
// .header(header::AUTHORIZATION, format!("Bearer {}", self.token))
// .send()
// }
fn put(&self, path: &str, body: String) -> reqwest::Response {
let path = self.homeserver_uri.clone() + path;
self.http_client
.put(Url::parse(&path).expect(&format!("Invalid path: {}", path)))
.header(header::AUTHORIZATION, format!("Bearer {}", self.token))
.header(header::CONTENT_TYPE, "application/json")
.body(body)
.send()
.unwrap()
}
}
#[derive(Debug)]
pub struct Matrix {
client: MatrixClient,
config: MatrixConfig,
}
#[derive(Debug, Deserialize)]
struct MatrixConfig {
homeserver_url: String,
token: String,
room_id: String,
}
impl Output for Matrix {
fn new(config: serde_yaml::Value) -> Result<Self, Box<dyn std::error::Error>> {
let config: MatrixConfig = serde_yaml::from_value(config)?;
Ok(Matrix {
client: MatrixClient::new(&config.homeserver_url, &config.token),
config: config,
})
}
fn process_message(&mut self, message: Message) {
use serde_json::json;
let msg_type = match message.level {
Level::Debug | Level::Notice => "m.notice",
_ => "m.text",
};
let decorator = match message.level {
Level::Debug | Level::Notice => ("", ""),
Level::Anomaly => ("<font color=\"#FFFF00\">", "</font>"),
Level::Issue => ("<font color=\"#FF0000\">", "</font>"),
Level::Critical => ("<strong><font color=\"#FF0000\">", "</font></strong>"),
};
let text = match message.level {
Level::Critical => format!("@room {}", message.text),
_ => message.text,
};
let body = json!({
"body": format!("[{}->{}]: {}", message.emitter, message.msg_type, text),
"formatted_body": format!("[{}->{}]: {}{}{}", message.emitter, message.msg_type, decorator.0, text, decorator.1),
"msgtype": msg_type,
"format": "org.matrix.custom.html",
})
.to_string();
let mut res = self.client.put(
&format!(
"/_matrix/client/r0/rooms/{}/send/m.room.message/{}",
utf8_percent_encode(&self.config.room_id, DEFAULT_ENCODE_SET),
utf8_percent_encode(
&rand::thread_rng()
.sample_iter(&Alphanumeric)
.take(32)
.collect::<String>(),
DEFAULT_ENCODE_SET
),
),
body,
);
if res.status().as_u16() != 200 {
log::warn!("Could not send Matrix message: {}", res.text().unwrap());
}
}
}

View File

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

View File

@ -5,9 +5,7 @@ pub use crate::output::*;
pub struct Stdout {} pub struct Stdout {}
impl Output for Stdout { impl Output for Stdout {
fn new( fn new(_config: serde_yaml::Value) -> Result<Self, Box<dyn std::error::Error>> {
_config: &HashMap<String, serde_yaml::Value>,
) -> Result<Self, Box<dyn std::error::Error>> {
Ok(Stdout {}) Ok(Stdout {})
} }
fn process_message(&mut self, message: Message) { fn process_message(&mut self, message: Message) {