#[macro_use]
extern crate log;
extern crate yaml_rust;
extern crate hyper;
extern crate rifling;
extern crate run_script;
use std::collections::HashMap;
use std::error::Error;
use std::fs::File;
use std::io::prelude::*;
use std::io::BufReader;
use std::net::SocketAddr;
use std::thread;
use hyper::rt::{run, Future};
use hyper::Server;
use run_script::ScriptOptions;
use yaml_rust::{Yaml, YamlLoader};
use rifling::hook::HookFunc;
use rifling::{Constructor, Delivery, DeliveryType, Hook};
macro_rules! get_value {
($source:expr) => {
match $source {
Some(string) => string.as_str(),
None => "unknown",
}
};
}
const SETTINGS: &str = "settings";
const KOTOMEI: &str = "kotomei";
const EVENTS: &str = "events";
const EVENTS_COMMON: &str = "common";
const EVENTS_ELSE: &str = "else";
const EVENTS_ALL: &str = "all";
#[derive(Clone)]
pub struct Handler {
config: Yaml,
}
impl Handler {
fn new(config: Yaml) -> Handler {
Handler { config }
}
fn process_commands(&self, event: &str, delivery: &Delivery) -> Option<String> {
let common_command = self.config[EVENTS][EVENTS_COMMON].as_str().unwrap_or("");
if let Some(command) = self.config[EVENTS][event].as_str() {
let mut exec = String::from(command);
exec = format!("{}\n{}", &common_command, &exec);
exec = exec.replace(
"{source}",
format!("{:?}", &delivery.delivery_type).as_str(),
);
exec = exec.replace("{id}", get_value!(&delivery.id));
exec = exec.replace("{event}", get_value!(&delivery.event));
exec = exec.replace("{signature}", get_value!(&delivery.signature));
exec = exec.replace("{payload}", get_value!(&delivery.unparsed_payload));
exec = exec.replace("{request_body}", get_value!(&delivery.request_body));
Some(exec)
} else {
None
}
}
}
impl HookFunc for Handler {
fn run(&self, delivery: &Delivery) {
let event = get_value!(&delivery.event);
info!(
"Received \"{}\" event from {:?}",
&event, &delivery.delivery_type
);
match &delivery.delivery_type {
DeliveryType::GitHub => {
let id = get_value!(&delivery.id);
info!("Delivery ID: \"{}\"", id);
}
_ => {
info!(
"Delivery ID not available for requests from {:?}",
&delivery.delivery_type
);
}
}
let mut commands_all: HashMap<String, Option<String>> = HashMap::new();
commands_all.insert(
EVENTS_ALL.into(),
self.process_commands(EVENTS_ALL, &delivery),
);
if let Some(command) = self.process_commands(event, &delivery) {
commands_all.insert(event.into(), Some(command));
} else {
commands_all.insert(
EVENTS_ELSE.into(),
self.process_commands(EVENTS_ELSE, &delivery),
);
}
for (section_name, command) in commands_all {
if let Some(exec) = command {
info!("Running commands in \"{}\" section", §ion_name);
debug!("Parsed command: {}", &exec);
let mut options = ScriptOptions::new();
options.capture_output = self.config[SETTINGS]["capture_output"]
.as_bool()
.unwrap_or(false);
options.exit_on_error = self.config[SETTINGS]["exit_on_error"]
.as_bool()
.unwrap_or(false);
options.print_commands = self.config[SETTINGS]["print_commands"]
.as_bool()
.unwrap_or(false);
debug!("Executor option: {:#?}", &options);
let args = vec![];
thread::spawn(move || {
let (code, output, error) = run_script::run(&exec.as_str(), &args, &options)
.expect("Failed to execute command");
info!("Commands in \"{}\" section exited with code {}", §ion_name, code);
if options.capture_output {
debug!("stdout:\n{}", output);
debug!("stderr:\n{}", error);
} else {
debug!("Output not captured.");
}
});
}
}
info!("Returning 200");
}
}
pub fn start(config_filename: &str) -> Result<(), Box<dyn Error>> {
debug!("Setting up...");
let mut config_content = String::new();
let config_file = File::open(config_filename)?;
let mut buf_reader = BufReader::new(config_file);
buf_reader.read_to_string(&mut config_content)?;
debug!(
"Got config:\n\"\"\"\n{}\n\"\"\"\nfrom file {}",
config_content, config_filename
);
let config = &YamlLoader::load_from_str(config_content.as_str())?[0];
debug!("Config parsed: {:?}", config);
let secret = if let Some(secret) = config[SETTINGS]["secret"].as_str() {
Some(String::from(secret))
} else {
None
};
if config[SETTINGS][KOTOMEI].as_bool().unwrap_or(true) {
warn!("If you ever see @kotomei2, tell him to study for the exam!");
}
let handler = Handler::new(config.clone());
let mut cons = Constructor::new();
let hook = Hook::new("*", secret, handler);
cons.register(hook);
let addr: SocketAddr = config[SETTINGS]["host"]
.as_str()
.expect("Unable to read host address")
.parse()
.expect("Unable to parse host address");
let ip_type = if addr.is_ipv4() { "IPv4" } else { "IPv6" };
info!(
"Listening on {} address {}:{}",
ip_type,
&addr.ip(),
&addr.port()
);
let server = Server::bind(&addr)
.serve(cons)
.map_err(|e| error!("Error: {:?}", e));
info!("Started");
run(server);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use rifling::handler::ContentType;
use rifling::handler::DeliveryType;
#[test]
fn command_generation() {
let test_config = r#"
settings:
host: 0.0.0.0:4567
secret: "secret"
events:
common: common
all: all
push: push
else: else
"#;
let config = &YamlLoader::load_from_str(test_config).unwrap()[0];
let handler = Handler::new(config.clone());
let delivery = Delivery::new(
DeliveryType::GitHub,
Some(String::from("unknown")),
Some(String::from("push")),
None,
ContentType::JSON,
Some(String::from(r#"{ zen: "test" }"#)),
);
let result_all = handler.process_commands(EVENTS_ALL, &delivery);
assert_eq!(
"\
common
all\
",
result_all.unwrap().as_str()
);
let result_push = handler.process_commands("push", &delivery);
assert_eq!(
"\
common
push\
",
result_push.unwrap().as_str()
);
let result_else = handler.process_commands(EVENTS_ELSE, &delivery);
assert_eq!(
"\
common
else\
",
result_else.unwrap().as_str()
);
}
}