changeset 17:4caa98d804bd draft

Implement filter rules
author Lewin Bormann <lbo@spheniscida.de>
date Sat, 03 Dec 2016 14:17:18 +0100
parents 4185ac5ed03d
children 08d04d44f86d
files src/config.rs
diffstat 1 files changed, 245 insertions(+), 11 deletions(-) [+]
line wrap: on
line diff
--- a/src/config.rs	Sat Dec 03 14:17:02 2016 +0100
+++ b/src/config.rs	Sat Dec 03 14:17:18 2016 +0100
@@ -1,24 +1,98 @@
 //! See prototype.toml for a full example config file.
 //!
 
+use std::collections::HashMap;
+use std::collections::btree_map::Entry;
+use std::default::Default;
 use std::net::SocketAddr;
 
+use dns_lookup;
+use toml;
+
+use priority;
+
+// helper functions for extracting values from TOML values
+fn get_string(tbl: &toml::Table, key: &str, default: String) -> String {
+    if let Some(e) = tbl.get(key) {
+        if let &toml::Value::String(ref s) = e {
+            return s.clone();
+        }
+    }
+    default
+}
+
+fn get_int(tbl: &toml::Table, key: &str, default: i64) -> i64 {
+    if let Some(e) = tbl.get(key) {
+        if let &toml::Value::Integer(i) = e {
+            return i;
+        }
+    }
+    default
+}
+
+fn tomlerr<T>(s: String) -> Result<T, toml::Error> {
+    Err(toml::Error::Custom(s))
+}
+
+// config structs
+
 #[derive(Clone,Default)]
 pub struct General {
     pub bind_path: String,
     pub max_msg_len: usize,
 }
 
+fn assemble_general(t: &toml::Table) -> Result<General, toml::Error> {
+    let default_bind_path = "/dev/log".to_string();
+    let default_max_msg_len = 8192;
+
+    let mut g = General {
+        bind_path: get_string(t, "bind_path", default_bind_path),
+        max_msg_len: get_int(t, "max_msg_len", default_max_msg_len) as usize,
+    };
+
+    Ok(g)
+}
+
 #[derive(Clone)]
 pub struct Remote {
-    addr: SocketAddr
+    addr: SocketAddr,
+}
+
+/// Assemble a Remote from a `remote` entry, and resolve the address if needed.
+fn assemble_remote(t: &toml::Table, name: &str) -> Result<Remote, toml::Error> {
+    let addr = get_string(t, "addr", String::new());
+    let port = get_int(t, "port", 0);
+
+    if !addr.is_empty() {
+        if let Ok(mut ips) = dns_lookup::lookup_host(&addr) {
+            if let Some(Ok(ip)) = ips.next() {
+                return Ok(Remote { addr: SocketAddr::new(ip, port as u16) });
+            }
+        }
+    }
+
+    Err(toml::Error::Custom(format!("Couldn't parse/resolve host {} for {}", addr, name)))
 }
 
 #[derive(Clone)]
 pub enum CompressType {
     NoCompression,
-    // NOTE: use flate2 for compression.
-    Gzip
+    // TODO: implement flate2 for compression.
+    Gzip,
+}
+
+fn assemble_compress_type(v: &toml::Value) -> Result<CompressType, toml::Error> {
+    if let &toml::Value::String(ref s) = v {
+        if s == "none" {
+            return Ok(CompressType::NoCompression);
+        } else if s == "gzip" {
+            return Ok(CompressType::Gzip);
+        } else {
+            return Err(toml::Error::Custom(format!("Unknown compression type {}", s)));
+        }
+    }
+    Err(toml::Error::Custom("Expected string for compression type".to_string()))
 }
 
 impl Default for CompressType {
@@ -39,27 +113,187 @@
     compress: CompressType,
 }
 
-#[derive(Clone)]
-pub enum Pattern {
+
+
+#[derive(Clone, Debug)]
+pub enum FacPattern {
     /// '*'
     Wildcard,
+    Facility(priority::Facility),
+    MultiFacility(Vec<priority::Facility>),
+}
+
+#[derive(Clone, Debug)]
+pub enum LvlPattern {
+    Wildcard,
+    /// Matches that level, or any lower (more important) level
+    Level(priority::Level),
+    /// Matches only that level
+    ExactLevel(priority::Level),
+    MultiLevel(Vec<priority::Level>),
+}
+
+impl Default for FacPattern {
+    fn default() -> FacPattern {
+        FacPattern::Wildcard
+    }
+}
+
+impl Default for LvlPattern {
+    fn default() -> LvlPattern {
+        LvlPattern::Wildcard
+    }
+}
+
+/// A Filter specifies one or more Facilities, and one or more Levels.
+type Filter = (FacPattern, LvlPattern);
+/// A Matcher is one or more Filters.
+type Matcher = Vec<Filter>;
+
+/// a pattern for a rule matches syslog messages based on level and facility. Valid patterns
+/// are:
+///
+/// *.* -- match all
+/// mail.* -- match all mail records
+/// mail,news.* -- match all mail and news records
+/// mail.warn,info -- match all mail records at warn or info
+/// mail,news.info,warn -- match all mail or news records at warn or info.
+/// mail.*;news,auth.info -- match all mail records and records for news or auth at info or higher
+/// (!) => if only a single level is given, this means "this level or more important"; if you only want to
+/// match that single level, use a rule like "mail.=info".
+fn parse_matcher(s: &str) -> Result<Matcher, toml::Error> {
+    let mut matcher = Vec::with_capacity(3);
+
+    for filter in s.trim().split(';') {
+        match parse_filter(filter) {
+            Ok(f) => matcher.push(f),
+            Err(e) => return Err(e),
+        }
+    }
+    Ok(matcher)
 }
 
-impl Default for Pattern {
-    fn default() -> Pattern {
-        Pattern::Wildcard
+fn parse_filter(s: &str) -> Result<Filter, toml::Error> {
+    let mut parts = s.trim().split('.');
+
+    if let Some(facilities) = parts.next() {
+        if let Some(levels) = parts.next() {
+            let fac_pattern = parse_facility_pattern(facilities);
+            let lvl_pattern = parse_level_pattern(levels);
+
+            return match (parse_facility_pattern(facilities), parse_level_pattern(levels)) {
+                (Ok(f), Ok(l)) => Ok((f, l)),
+                (Err(e), Err(f)) => tomlerr(format!("{} {}", e, f)),
+                (Ok(_), Err(e)) => tomlerr(format!("{}", e)),
+                (Err(e), Ok(_)) => tomlerr(format!("{}", e)),
+            };
+        }
+    }
+    tomlerr(format!("Bad filter pattern: {}", s))
+}
+
+fn parse_facility_pattern(s: &str) -> Result<FacPattern, toml::Error> {
+    let mut list: Vec<String> = s.trim().split(',').map(String::from).collect();
+
+    if list.len() > 1 {
+        let mut facilities = vec![priority::Facility::LOCAL0; list.len()];
+        let mut i = 0;
+
+        for f in list {
+            if let Some(fac) = priority::str_to_facility(f.trim()) {
+                facilities[i] = fac;
+            } else {
+                return tomlerr(format!("Unrecognized facility: {}", f));
+            }
+        }
+        Ok(FacPattern::MultiFacility(facilities))
+    } else if list.len() == 1 {
+        if let Some(fac) = priority::str_to_facility(&list[0]) {
+            Ok(FacPattern::Facility(fac))
+        } else {
+            tomlerr(format!("Unknown facility: {}", &list[0]))
+        }
+    } else {
+        return tomlerr(format!("Level pattern can't be empty"));
+    }
+}
+
+fn parse_level_pattern(s: &str) -> Result<LvlPattern, toml::Error> {
+    let mut list: Vec<String> = s.trim().split(',').map(String::from).collect();
+
+    if list.len() > 1 {
+        let mut levels = vec![priority::Level::NONE; list.len()];
+        let mut i = 0;
+
+        for l in list {
+            if let Some(lvl) = priority::str_to_level(l.trim()) {
+                levels[i] = lvl;
+            } else {
+                return tomlerr(format!("Unrecognized facility: {}", l));
+            }
+        }
+        Ok(LvlPattern::MultiLevel(levels))
+    } else if list.len() == 1 {
+        let l = &list[0];
+
+        // exact rule
+        if &l[0..1] == "=" {
+            if let Some(fac) = priority::str_to_level(&l[1..]) {
+                Ok(LvlPattern::ExactLevel(fac))
+            } else {
+                tomlerr(format!("Unknown exact facility: {}", l))
+            }
+        } else if let Some(lvl) = priority::str_to_level(l) {
+            Ok(LvlPattern::Level(lvl))
+        } else {
+            tomlerr(format!("Unknown facility: {}", &list[0]))
+        }
+    } else {
+        return tomlerr(format!("Level pattern can't be empty"));
     }
 }
 
 #[derive(Clone,Default)]
 pub struct Rule {
-    level_pattern: Pattern,
-    // referes to File.name
-    dest: String,
+    pattern: Matcher,
+    // refers to the file entry name, not the file name.
+    file: String,
+    remote_dest: String,
+    stop: bool,
 }
 
 #[derive(Clone,Default)]
 pub struct Config {
     pub general: General,
+    remotes: HashMap<String, Remote>,
+    files: HashMap<String, File>,
+    rules: Vec<Rule>,
+}
+
+fn decode_config(text: &String) -> Result<Config, toml::Error> {
+    if let Some(tbl) = toml::Parser::new(&text).parse() {
+        let general = try!(assemble_general(&tbl));
+
+        unimplemented!()
+    } else {
+        return Err(toml::Error::Custom("Couldn't parse configuration".to_string()));
+    }
 }
 
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_config_matcher_parse() {
+        let m = "mail,news.info,warn;kern.=warn;syslog.warn;daemon.debug,info,warn";
+        assert_eq!(format!("{:?}", super::parse_matcher(m)),
+                   "Ok([(MultiFacility([NEWS, LOCAL0]), MultiLevel([WARNING, NONE])), \
+                    (Facility(KERN), ExactLevel(WARNING)), (Facility(SYSLOG), Level(WARNING)), \
+                    (Facility(DAEMON), MultiLevel([WARNING, NONE, NONE]))])");
+
+        let m = "mail, news.info; kern.warn";
+
+        assert_eq!(format!("{:?}", super::parse_matcher(m)), "Ok([(MultiFacility([NEWS, LOCAL0]), Level(INFO)), (Facility(KERN), Level(WARNING))])");
+    }
+}