view src/config.rs @ 17:4caa98d804bd draft

Implement filter rules
author Lewin Bormann <lbo@spheniscida.de>
date Sat, 03 Dec 2016 14:17:18 +0100
parents 2d2296e5bcf4
children 4914bd1e162f
line wrap: on
line source

//! 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,
}

/// 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,
    // 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 {
    fn default() -> CompressType {
        CompressType::NoCompression
    }
}

#[derive(Clone,Default)]
pub struct File {
    name: String,
    location: String,
    /// bytes
    max_size: usize,
    /// seconds
    max_age: u64,
    history: i32,
    compress: CompressType,
}



#[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)
}

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 {
    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))])");
    }
}