diff --git a/Cargo.lock b/Cargo.lock index adb46e8..52fc786 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,32 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] + +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + [[package]] name = "autocfg" version = "1.0.1" @@ -57,6 +83,46 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +dependencies = [ + "libc", + "num-integer", + "num-traits 0.2.14", + "time", + "winapi", +] + +[[package]] +name = "colored" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3616f750b84d8f0de8a58bda93e08e2a81ad3f523089b05f1dffecab48c6cbd" +dependencies = [ + "atty", + "lazy_static", + "winapi", +] + +[[package]] +name = "config" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1b9d958c2b1368a663f05538fc1b5975adce1e19f435acceae987aceeeb369" +dependencies = [ + "lazy_static", + "nom", + "rust-ini", + "serde 1.0.127", + "serde-hjson", + "serde_json", + "toml", + "yaml-rust", +] + [[package]] name = "cpufeatures" version = "0.1.5" @@ -75,6 +141,35 @@ dependencies = [ "generic-array", ] +[[package]] +name = "dirs" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30baa043103c9d0c2a57cf537cc2f35623889dc0d405e6c3cccfadbc81c71309" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03d86534ed367a67548dc68113a0f5db55432fdfbb6e6f9d77704397d95d5780" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "fern" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9a4820f0ccc8a7afd67c39a0f1a0f4b07ca1725164271a64939d7aeb9af065" +dependencies = [ + "log", +] + [[package]] name = "fnv" version = "1.0.7" @@ -186,6 +281,12 @@ dependencies = [ "wasi 0.10.2+wasi-snapshot-preview1", ] +[[package]] +name = "glob" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" + [[package]] name = "h2" version = "0.3.3" @@ -345,12 +446,31 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "lexical-core" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe" +dependencies = [ + "arrayvec", + "bitflags", + "cfg-if", + "ryu", + "static_assertions", +] + [[package]] name = "libc" version = "0.2.98" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320cfe77175da3a483efed4bc0adc1968ca050b098ce4f2f1c13a56626128790" +[[package]] +name = "linked-hash-map" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" + [[package]] name = "log" version = "0.4.14" @@ -414,8 +534,18 @@ dependencies = [ name = "multihook" version = "0.1.0" dependencies = [ + "chrono", + "colored", + "config", + "dirs", + "fern", + "glob", + "lazy_static", + "log", + "serde 1.0.127", "thiserror", "tokio", + "toml", "warp", ] @@ -437,6 +567,17 @@ dependencies = [ "twoway", ] +[[package]] +name = "nom" +version = "5.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af" +dependencies = [ + "lexical-core", + "memchr", + "version_check", +] + [[package]] name = "ntapi" version = "0.3.6" @@ -446,6 +587,34 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-integer" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +dependencies = [ + "autocfg", + "num-traits 0.2.14", +] + +[[package]] +name = "num-traits" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31" +dependencies = [ + "num-traits 0.2.14", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg", +] + [[package]] name = "num_cpus" version = "1.13.0" @@ -626,6 +795,33 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_users" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" +dependencies = [ + "getrandom 0.2.3", + "redox_syscall", +] + +[[package]] +name = "regex" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + [[package]] name = "remove_dir_all" version = "0.5.3" @@ -635,6 +831,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "rust-ini" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e52c148ef37f8c375d49d5a73aa70713125b7f19095948a923f80afdeb22ec2" + [[package]] name = "ryu" version = "1.0.5" @@ -653,11 +855,43 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" +[[package]] +name = "serde" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dad3f759919b92c3068c696c15c3d17238234498bbdcc80f2c469606f948ac8" + [[package]] name = "serde" version = "1.0.127" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f03b9878abf6d14e6779d3f24f07b2cfa90352cfec4acc5aab8f1ac7f146fae8" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-hjson" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a3a4e0ea8a88553209f6cc6cfe8724ecad22e1acf372793c27d995290fe74f8" +dependencies = [ + "lazy_static", + "num-traits 0.1.43", + "regex", + "serde 0.8.23", +] + +[[package]] +name = "serde_derive" +version = "1.0.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a024926d3432516606328597e0f224a51355a493b49fdd67e9209187cbe55ecc" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "serde_json" @@ -667,7 +901,7 @@ checksum = "336b10da19a12ad094b59d870ebde26a45402e5b470add4b5fd03c5048a32127" dependencies = [ "itoa", "ryu", - "serde", + "serde 1.0.127", ] [[package]] @@ -679,7 +913,7 @@ dependencies = [ "form_urlencoded", "itoa", "ryu", - "serde", + "serde 1.0.127", ] [[package]] @@ -720,6 +954,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "syn" version = "1.0.74" @@ -858,6 +1098,15 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +dependencies = [ + "serde 1.0.127", +] + [[package]] name = "tower-service" version = "0.3.1" @@ -1010,7 +1259,7 @@ dependencies = [ "percent-encoding", "pin-project", "scoped-tls", - "serde", + "serde 1.0.127", "serde_json", "serde_urlencoded", "tokio", @@ -1054,3 +1303,12 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] diff --git a/Cargo.toml b/Cargo.toml index f118b4a..919706b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,19 @@ edition = "2018" [dependencies] warp = "0.3.1" thiserror = "1.0.26" +config = "0.11.0" +lazy_static = "1.4.0" +dirs = "3.0.2" +toml = "0.5.8" +glob = "0.3.0" +log = "0.4.14" +colored = "2.0.0" +chrono = "0.4.19" +fern = "0.6.0" + +[dependencies.serde] +version = "1.0.127" +features = ["derive"] [dependencies.tokio] version = "1.9.0" diff --git a/src/action.rs b/src/action.rs index d160cc8..af5935c 100644 --- a/src/action.rs +++ b/src/action.rs @@ -1,9 +1,59 @@ use crate::error::MultihookResult; +use std::path::{Path, PathBuf}; +use tokio::process::Command; -pub enum HookAction {} +pub enum HookAction { + Script(PathBuf), + Command(String), +} impl HookAction { pub async fn execute(&self, body: &str) -> MultihookResult<()> { + match self { + HookAction::Script(s) => Self::execute_script(s, body).await, + HookAction::Command(c) => Self::execute_command(c, body).await, + } + } + + async fn execute_command(command: &str, body: &str) -> MultihookResult<()> { + let output = Command::new("sh") + .env("HOOK_BODY", body) + .arg("-c") + .arg(command) + .output() + .await?; + let stderr = String::from_utf8_lossy(&output.stderr[..]); + let stdout = String::from_utf8_lossy(&output.stdout[..]); + log::debug!("Command output is: {}", stdout); + + if stderr.len() > 0 { + log::error!("Errors occurred during command execution: {}", stderr); + } + Ok(()) } + + async fn execute_script(script: &PathBuf, body: &str) -> MultihookResult<()> { + let output = Command::new(script).env("HOOK_BODY", body).output().await?; + let stderr = String::from_utf8_lossy(&output.stderr[..]); + let stdout = String::from_utf8_lossy(&output.stdout[..]); + log::debug!("Script output is: {}", stdout); + + if stderr.len() > 0 { + log::error!("Errors occurred during script execution: {}", stderr); + } + + Ok(()) + } +} + +impl From for HookAction { + fn from(action: String) -> Self { + let path = PathBuf::from(&action); + if Path::new(&path).exists() { + Self::Script(path) + } else { + Self::Command(action) + } + } } diff --git a/src/error.rs b/src/error.rs index 71ebb21..06efb40 100644 --- a/src/error.rs +++ b/src/error.rs @@ -12,8 +12,17 @@ pub enum MultihookError { #[error("Failed to parse body as utf8 string {0}")] UTF8Error(#[from] FromUtf8Error), - #[error("Unknown endpoint")] - UnknownEndpoint, + #[error(transparent)] + TomlSerializeError(#[from] toml::ser::Error), + + #[error(transparent)] + TomlDeserializeError(#[from] toml::de::Error), + + #[error(transparent)] + IoError(#[from] std::io::Error), + + #[error(transparent)] + ConfigError(#[from] config::ConfigError), } impl Reject for MultihookError {} diff --git a/src/logging.rs b/src/logging.rs new file mode 100644 index 0000000..55af8ba --- /dev/null +++ b/src/logging.rs @@ -0,0 +1,56 @@ +use std::str::FromStr; + +use chrono::Local; +use colored::*; +use log::{Level, LevelFilter}; + +/// Initializes the env_logger with a custom format +/// that also logs the thread names +pub fn init_logger() { + fern::Dispatch::new() + .format(|out, message, record| { + let color = get_level_style(record.level()); + let mut target = record.target().to_string(); + target.truncate(39); + + out.finish(format_args!( + "{:<40}| {} {}: {}", + target.dimmed().italic(), + Local::now().format("%Y-%m-%dT%H:%M:%S"), + record + .level() + .to_string() + .to_lowercase() + .as_str() + .color(color), + message + )) + }) + .level( + std::env::var("RUST_LOG") + .ok() + .and_then(|level| log::LevelFilter::from_str(&level).ok()) + .unwrap_or(LevelFilter::Info), + ) + .level_for("tokio", log::LevelFilter::Info) + .level_for("tracing", log::LevelFilter::Warn) + .level_for("rustls", log::LevelFilter::Warn) + .level_for("h2", log::LevelFilter::Warn) + .level_for("hyper", log::LevelFilter::Warn) + .level_for("tokio_util", log::LevelFilter::Warn) + .level_for("want", log::LevelFilter::Warn) + .level_for("mio", log::LevelFilter::Warn) + .chain(std::io::stdout()) + .apply() + .expect("failed to init logger"); +} + +fn get_level_style(level: Level) -> colored::Color { + match level { + Level::Trace => colored::Color::Magenta, + Level::Debug => colored::Color::Blue, + Level::Info => colored::Color::Green, + Level::Warn => colored::Color::Yellow, + Level::Error => colored::Color::Red, + } +} diff --git a/src/main.rs b/src/main.rs index 2e7643a..76f4638 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,22 @@ -mod server; +use crate::logging::init_logger; +use crate::server::HookServer; +use crate::settings::get_settings; + mod action; mod error; +mod logging; +mod server; +mod settings; + +#[tokio::main] +async fn main() { + init_logger(); + let settings = get_settings(); + let mut server = HookServer::new(); + for (name, endpoint) in &settings.endpoints { + log::info!("Adding endpoint {} with path {}", name, &endpoint.path); + server.add_hook(endpoint.path.clone(), endpoint.action.clone().into()) + } -fn main() { - println!("Hello, world!"); + server.start(&settings.server.address).await } diff --git a/src/server.rs b/src/server.rs index 7d20876..32fe35f 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,7 +1,9 @@ use crate::action::HookAction; -use crate::error::{MultihookError, MultihookResult}; +use crate::error::MultihookError; use std::collections::HashMap; +use std::net::SocketAddr; use std::sync::Arc; +use warp::http::Response; use warp::hyper::body::Bytes; use warp::{Filter, Rejection}; @@ -10,6 +12,12 @@ pub struct HookServer { } impl HookServer { + pub fn new() -> Self { + Self { + endpoints: Default::default(), + } + } + pub fn add_hook(&mut self, point: String, action: HookAction) { self.endpoints.insert(point, action); } @@ -18,13 +26,24 @@ impl HookServer { body: Bytes, point: String, action: Arc, - ) -> Result { + ) -> Result, Rejection> { let body = String::from_utf8(body.to_vec()).map_err(MultihookError::from)?; action.execute(&body).await?; - Ok(format!("Hook {} executed", point)) + log::info!("Hook '{}' executed", point); + Ok(Response::builder() + .body(format!("Hook '{}' executed", point)) + .unwrap()) + } + + async fn not_found_response() -> Result, Rejection> { + log::info!("Endpoint not found"); + Ok(Response::builder() + .status(404) + .body(String::from("Endpoint not found")) + .unwrap()) } - pub fn start(self) { + pub async fn start(self, address: &str) { let routes = self .endpoints .into_iter() @@ -35,15 +54,25 @@ impl HookServer { .and(warp::body::bytes()) .and_then(move |b| { let action = Arc::clone(&action); - Self::execute_action(b, point.clone(), action) + let point = point.clone(); + async move { Self::execute_action(b, point, action).await } }) - .map(|_| warp::reply()) .boxed() }) - .fold(warp::any().map(warp::reply).boxed(), |routes, route| { - routes.or(route).unify().boxed() - }); + .fold( + warp::get() + .and_then(|| async { Self::not_found_response().await }) + .boxed(), + |routes, route| routes.or(route).unify().boxed(), + ); - let routes = warp::serve(routes); + log::info!("Starting server on {}", address); + warp::serve(routes) + .bind( + address + .parse::() + .expect("Invalid address in settings"), + ) + .await; } } diff --git a/src/settings.rs b/src/settings.rs new file mode 100644 index 0000000..1adb01a --- /dev/null +++ b/src/settings.rs @@ -0,0 +1,80 @@ +use crate::error::MultihookResult; +use config::File; +use lazy_static::lazy_static; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::fs; +use std::path::{Path, PathBuf}; + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct Settings { + pub server: ServerSettings, + pub endpoints: HashMap, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct ServerSettings { + pub address: String, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct EndpointSettings { + pub path: String, + pub action: String, +} + +impl Default for Settings { + fn default() -> Self { + Self { + endpoints: HashMap::new(), + server: ServerSettings { + address: String::from("127.0.0.1:8080"), + }, + } + } +} + +pub fn get_settings() -> &'static Settings { + lazy_static! { + static ref SETTINGS: Settings = load_settings().expect("Failed to get settings"); + } + + &*SETTINGS +} + +fn load_settings() -> MultihookResult { + let config_dir = dirs::config_dir() + .unwrap_or(PathBuf::from(".config")) + .join("multihook"); + if !Path::new(&config_dir).exists() { + fs::create_dir(&config_dir)?; + } + write_toml_pretty( + &config_dir.clone().join("default-config.toml"), + &Settings::default(), + )?; + + let mut settings = config::Config::default(); + settings + .merge( + glob::glob(&format!("{}/*.toml", config_dir.to_string_lossy())) + .unwrap() + .map(|path| File::from(path.unwrap())) + .collect::>(), + )? + .merge(config::Environment::with_prefix("MULTIHOOK"))?; + + let settings: Settings = settings.try_into()?; + + Ok(settings) +} + +fn write_toml_pretty(path: &PathBuf, value: &T) -> MultihookResult<()> { + let mut buf_str = String::new(); + let mut serializer = toml::Serializer::pretty(&mut buf_str); + serializer.pretty_array(true); + value.serialize(&mut serializer)?; + fs::write(path, buf_str.as_bytes())?; + + Ok(()) +}