diff --git a/Cargo.lock b/Cargo.lock index 3c9ca43..7c41c95 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -192,6 +192,7 @@ dependencies = [ "hmac 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)", "hyper 0.12.8 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "mktemp 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", "notify 4.0.4 (registry+https://github.com/rust-lang/crates.io-index)", "regex 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", "secstr 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -504,6 +505,14 @@ dependencies = [ "ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "mktemp" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "uuid 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "net2" version = "0.2.33" @@ -569,6 +578,16 @@ dependencies = [ "proc-macro2 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "rand" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "rand" version = "0.4.2" @@ -617,6 +636,11 @@ name = "rustc-demangle" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "rustc-serialize" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "rustc_version" version = "0.2.3" @@ -960,6 +984,15 @@ name = "utf8-ranges" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "uuid" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rand 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "vec_map" version = "0.8.1" @@ -1090,6 +1123,7 @@ dependencies = [ "checksum mio 0.6.15 (registry+https://github.com/rust-lang/crates.io-index)" = "4fcfcb32d63961fb6f367bfd5d21e4600b92cd310f71f9dca25acae196eb1560" "checksum miow 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "3e690c5df6b2f60acd45d56378981e827ff8295562fc8d34f573deb267a59cd1" "checksum miow 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1f2f3b1cf331de6896aabf6e9d55dca90356cc9960cca7eaaf408a355ae919" +"checksum mktemp 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "77001ceb9eed65439f3dc2a2543f9ba1417d912686bf224a7738d0966e6dcd69" "checksum net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)" = "42550d9fb7b6684a6d404d9fa7250c2eb2646df731d1c06afc06dcee9e1bcf88" "checksum nix 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "bfb3ddedaa14746434a02041940495bf11325c22f6d36125d3bdd56090d50a79" "checksum nodrop 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "9a2228dca57108069a5262f2ed8bd2e82496d2e074a06d1ccc7ce1687b6ae0a2" @@ -1097,12 +1131,14 @@ dependencies = [ "checksum num_cpus 1.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c51a3322e4bca9d212ad9a158a02abc6934d005490c054a2778df73a70aa0a30" "checksum proc-macro2 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)" = "762eea716b821300a86da08870a64b597304866ceb9f54a11d67b4cf56459c6a" "checksum quote 0.6.6 (registry+https://github.com/rust-lang/crates.io-index)" = "ed7d650913520df631972f21e104a4fa2f9c82a14afc65d17b388a2e29731e7c" +"checksum rand 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)" = "15a732abf9d20f0ad8eeb6f909bf6868722d9a06e1e50802b6a70351f40b4eb1" "checksum rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "eba5f8cb59cc50ed56be8880a5c7b496bfd9bd26394e176bc67884094145c2c5" "checksum redox_syscall 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)" = "c214e91d3ecf43e9a4e41e578973adeb14b474f2bee858742d127af75a0112b1" "checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76" "checksum regex 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "5bbbea44c5490a1e84357ff28b7d518b4619a159fed5d25f6c1de2d19cc42814" "checksum regex-syntax 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "747ba3b235651f6e2f67dfa8bcdcd073ddb7c243cb21c442fc12395dfcac212d" "checksum rustc-demangle 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "bcfe5b13211b4d78e5c2cadfebd7769197d95c639c35a50057eb4c05de811395" +"checksum rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)" = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda" "checksum rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" "checksum ryu 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "16aa12da69951804cddf5f74d96abcc414a31b064e610dc81e37c1536082f491" "checksum same-file 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "cfb6eded0b06a0b512c8ddbcf04089138c9b4362c2f696f3c3d76039d68f3637" @@ -1144,6 +1180,7 @@ dependencies = [ "checksum unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" "checksum unreachable 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56" "checksum utf8-ranges 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "662fab6525a98beff2921d7f61a39e7d59e0b425ebc7d0d9e66d316e55124122" +"checksum uuid 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)" = "78c590b5bd79ed10aad8fb75f078a59d8db445af6c743e55c4a53227fc01c13f" "checksum vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a" "checksum version_check 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "7716c242968ee87e5542f8021178248f267f295a5c4803beae8b8b7fd9bc6051" "checksum void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" diff --git a/Cargo.toml b/Cargo.toml index 38924da..b0c4e6d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ sha-1 = "0.7" failure = "0.1" futures = "0.1" hyper = "0.12" +mktemp = "0.3" lazy_static = "1.1" notify = "4.0" regex = "1.0" diff --git a/README.md b/README.md index b225758..0a502c9 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,17 @@ Dip Configurable webhook server. -Read documentation at `cargo doc`. +Express your webhooks in terms of composable blocks such as: + +```toml +[[handlers]] +type = "github" +secret = "hunter2" + +[[handlers]] +type = "command" +command = "cargo build" +``` Contact ------- diff --git a/examples/github.rs b/examples/github.rs index 21c10e9..2574d9f 100644 --- a/examples/github.rs +++ b/examples/github.rs @@ -11,9 +11,11 @@ extern crate generic_array; extern crate structopt; use std::collections::HashMap; +use std::env; use std::io::{self, Read}; use std::iter::FromIterator; use std::path::PathBuf; +use std::process::Command; use failure::{err_msg, Error}; use generic_array::GenericArray; @@ -29,10 +31,13 @@ struct Opt { pub config: String, } -#[derive(Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize)] struct Config { secret: String, - outdir: PathBuf, + #[serde(default)] + disable_hmac_verify: bool, + #[serde(default = "default_path")] + path: PathBuf, } #[derive(Serialize, Deserialize)] @@ -41,36 +46,60 @@ struct Payload { headers: HashMap, } +#[derive(Debug, Serialize, Deserialize)] +struct RepositoryInfo { + clone_url: String, +} + +#[derive(Debug, Serialize, Deserialize)] +struct GithubPayload { + repository: RepositoryInfo, +} + +fn default_path() -> PathBuf { + PathBuf::from(".") +} + fn main() -> Result<(), Error> { let args = Opt::from_args(); let config: Config = serde_json::from_str(&args.config)?; + println!("{:?}", config); let mut payload = String::new(); io::stdin().read_to_string(&mut payload)?; - println!("raw payload: {}", payload); let payload: Payload = serde_json::from_str(&payload)?; - println!("processed payload: {}", payload.body); - let secret = GenericArray::from_iter(config.secret.bytes()); - let mut mac = Hmac::::new(&secret); - mac.input(payload.body.as_bytes()); - let signature = mac - .result() - .code() - .into_iter() - .map(|b| format!("{:02x}", b)) - .collect::>() - .join(""); + if !config.disable_hmac_verify { + let secret = GenericArray::from_iter(config.secret.bytes()); + let mut mac = Hmac::::new(&secret); + mac.input(payload.body.as_bytes()); + let signature = mac.result() + .code() + .into_iter() + .map(|b| format!("{:02x}", b)) + .collect::>() + .join(""); - let auth = payload - .headers - .get("x-hub-signature") - .ok_or(err_msg("Missing auth header"))?; + let auth = payload + .headers + .get("x-hub-signature") + .ok_or(err_msg("Missing auth header"))?; - let left = SecStr::from(format!("sha1={}", signature)); - let right = SecStr::from(auth.bytes().collect::>()); - assert!(left == right, "HMAC signature didn't match"); + let left = SecStr::from(format!("sha1={}", signature)); + let right = SecStr::from(auth.bytes().collect::>()); + assert!(left == right, "HMAC signature didn't match",); + } - println!("gonna clone it to {:?}", config.outdir); + let payload: GithubPayload = serde_json::from_str(&payload.body)?; + let mut target_path = PathBuf::from(env::var("DIP_WORKDIR")?); + target_path.push(&config.path); + Command::new("git") + .arg("clone") + .arg(&payload.repository.clone_url) + .arg("--recursive") + .arg("--depth") + .arg("1") + .arg(&target_path) + .output()?; Ok(()) } diff --git a/src/config.rs b/src/config.rs index 4d37664..fd8c5a5 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,8 +1,22 @@ use std::path::{Path, PathBuf}; +use failure::{err_msg, Error}; +use walkdir::WalkDir; + +use Hook; +use {HOOKS, PROGRAMS}; + +#[derive(Debug, StructOpt)] pub struct Config { + /// The root configuration directory for dip. This argument is required. + #[structopt(short = "d", long = "root", parse(from_os_str))] pub root: PathBuf, + /// A string containing the address to bind to. This defaults to "0.0.0.0:5000". + #[structopt(short = "b", long = "bind", default_value = "0.0.0.0:5000")] pub bind: String, + /// If a hook is specified here, it will be triggered manually exactly once and then the + /// program will exit rather than running as a server. + #[structopt(short = "h", long = "hook")] pub hook: Option, } @@ -26,3 +40,72 @@ impl Config { return self; } } + +pub fn load_config

(root: P) +where + P: AsRef, +{ + println!("Reloading config..."); + // hold on to the lock while config is being reloaded + { + let mut programs = PROGRAMS.lock().unwrap(); + // TODO: some kind of smart diff + programs.clear(); + let programs_dir = { + let mut p = root.as_ref().to_path_buf(); + p.push("handlers"); + p + }; + if programs_dir.exists() { + for entry in WalkDir::new(programs_dir) { + let path = match entry.as_ref().map(|e| e.path()) { + Ok(path) => path, + _ => continue, + }; + if !path.is_file() { + continue; + } + match path.file_name() + .and_then(|s| s.to_str()) + .ok_or(err_msg("???")) + .map(|s| { + let filename = s.to_owned(); + programs.insert(filename, path.to_path_buf()) + }) { + _ => (), // don't care + } + } + } + } + { + let mut hooks = HOOKS.lock().unwrap(); + hooks.clear(); + let hooks_dir = { + let mut p = root.as_ref().to_path_buf(); + p.push("hooks"); + p + }; + if hooks_dir.exists() { + for entry in WalkDir::new(hooks_dir) { + let path = match entry.as_ref().map(|e| e.path()) { + Ok(path) => path, + _ => continue, + }; + if !path.is_file() { + continue; + } + match (|path: &Path| -> Result<(), Error> { + let hook = Hook::from_file(path)?; + let name = hook.get_name(); + hooks.insert(name, hook); + Ok(()) + })(path) + { + Ok(_) => (), + Err(err) => eprintln!("Failed to read config from {:?}: {}", path, err), + } + } + } + } + println!("Done loading config."); +} diff --git a/src/handler.rs b/src/handler.rs index b01b3b6..b96035a 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -11,27 +11,52 @@ use PROGRAMS; pub struct Handler { config: TomlValue, - exec: PathBuf, + action: Action, +} + +pub enum Action { + Command(String), + Exec(PathBuf), } impl Handler { + pub fn config(&self) -> &TomlValue { + &self.config + } pub fn from(config: &TomlValue) -> Result { let handler = config .get("type") .ok_or(err_msg("No 'type' found."))? .as_str() .ok_or(err_msg("'type' is not a string."))?; - let exec = { - let programs = PROGRAMS.lock().unwrap(); - programs - .get(handler) - .ok_or(err_msg(format!("'{}' is not a valid executable", handler))) - .map(|value| value.clone())? + let action = match handler { + "command" => { + let command = config + .get("command") + .ok_or(err_msg("No 'command' found"))? + .as_str() + .ok_or(err_msg("'command' is not a string."))?; + Action::Command(command.to_owned()) + } + handler => { + let programs = PROGRAMS.lock().unwrap(); + let program = programs + .get(handler) + .ok_or(err_msg(format!("'{}' is not a valid executable", handler))) + .and_then(|value| { + value + .canonicalize() + .map_err(|_| err_msg("failed to canonicalize the path")) + }) + .map(|value| value.clone())?; + Action::Exec(program) + } }; let config = config.clone(); - Ok(Handler { config, exec }) + Ok(Handler { config, action }) } - pub fn run(&self, input: JsonValue) -> Result { + + pub fn run(&self, temp_path: &PathBuf, input: JsonValue) -> Result { let config = { let mut buf: Vec = Vec::new(); { @@ -41,33 +66,71 @@ impl Handler { String::from_utf8(buf).unwrap() }; - let mut child = Command::new(&self.exec) - .env("DIP_ROOT", "") - .arg("--config") - .arg(config) - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn()?; - { - match child.stdin { - Some(ref mut stdin) => { - write!(stdin, "{}", input)?; + let output = match &self.action { + Action::Command(ref cmd) => { + // TODO: allow some kind of simple variable replacement + let child = Command::new("/bin/bash") + .current_dir(&temp_path) + .env("DIP_ROOT", "lol") + .env("DIP_WORKDIR", temp_path) + .arg("-c") + .arg(cmd) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn()?; + let output = child.wait_with_output()?; + if !output.status.success() { + // TODO: get rid of unwraps + return Err(err_msg(format!( + "Command '{}' returned with a non-zero status code: {}\nstdout:\n{}\nstderr:\n{}", + cmd, + output.status, + String::from_utf8(output.stdout).unwrap_or_else(|_| String::new()), + String::from_utf8(output.stderr).unwrap_or_else(|_| String::new()) + ))); } - None => bail!("done fucked"), - }; - } - let output = child.wait_with_output()?; - if !output.status.success() { - // TODO: get rid of unwraps - return Err(err_msg(format!( - "'{:?}' returned with a non-zero status code: {}\nstdout:\n{}\nstderr:\n{}", - self.exec, - output.status, - String::from_utf8(output.stdout).unwrap(), - String::from_utf8(output.stderr).unwrap() - ))); - } - Ok(json!({})) + output + } + Action::Exec(ref path) => { + let mut child = Command::new(&path) + .current_dir(&temp_path) + .env("DIP_ROOT", "") + .env("DIP_WORKDIR", temp_path) + .arg("--config") + .arg(config) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn()?; + { + match child.stdin { + Some(ref mut stdin) => { + write!(stdin, "{}", input)?; + } + None => bail!("done fucked"), + }; + } + let output = child.wait_with_output()?; + if !output.status.success() { + // TODO: get rid of unwraps + return Err(err_msg(format!( + "'{:?}' returned with a non-zero status code: {}\nstdout:\n{}\nstderr:\n{}", + path, + output.status, + String::from_utf8(output.stdout).unwrap_or_else(|_| String::new()), + String::from_utf8(output.stderr).unwrap_or_else(|_| String::new()) + ))); + } + output + } + }; + + let stdout = String::from_utf8(output.stdout).unwrap_or_else(|_| String::new()); + let stderr = String::from_utf8(output.stderr).unwrap_or_else(|_| String::new()); + Ok(json!({ + "stdout": stdout, + "stderr": stderr, + })) } } diff --git a/src/hook.rs b/src/hook.rs index 0844f68..15673fb 100644 --- a/src/hook.rs +++ b/src/hook.rs @@ -1,10 +1,11 @@ use std::fs::File; use std::io::Read; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::slice::Iter; use failure::{err_msg, Error}; -use hyper::{Body, Request, Response}; +use hyper::StatusCode; +use serde_json::Value as JsonValue; use toml::Value; use Handler; @@ -31,8 +32,7 @@ impl Hook { where P: AsRef, { - let filename = path - .as_ref() + let filename = path.as_ref() .file_name() .ok_or(err_msg("what the fuck bro"))? .to_str() @@ -52,7 +52,30 @@ impl Hook { pub fn iter(&self) -> Iter { self.handlers.iter() } - pub fn handle(&self, _payload: &Request) -> Result, Error> { - Ok(Response::new(Body::from("lol"))) + pub fn handle( + &self, + req: JsonValue, + temp_path: &PathBuf, + ) -> Result<(StatusCode, String), Error> { + Ok(self.iter() + .fold(Ok(req), |prev, handler| { + prev.and_then(|val| { + println!("Running {}...", handler.config()); + let result = handler.run(&temp_path, val); + println!("{:?}", result); + result + }) + }) + .map(|res| { + ( + StatusCode::ACCEPTED, + format!( + "stdout:\n{}\n\nstderr:\n{}", + res.get("stdout").and_then(|v| v.as_str()).unwrap_or(""), + res.get("stderr").and_then(|v| v.as_str()).unwrap_or(""), + ), + ) + }) + .unwrap_or_else(|err| (StatusCode::BAD_REQUEST, format!("Error: {:?}", err)))) } } diff --git a/src/lib.rs b/src/lib.rs index 20d29bd..047a6aa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,14 +2,17 @@ #[macro_use] extern crate failure; -extern crate hyper; extern crate futures; +extern crate hyper; +extern crate mktemp; extern crate serde; #[macro_use] extern crate serde_json; #[macro_use] extern crate lazy_static; extern crate notify; +#[macro_use] +extern crate structopt; extern crate regex; extern crate toml; extern crate walkdir; @@ -17,156 +20,38 @@ extern crate walkdir; pub mod config; pub mod handler; pub mod hook; +pub mod service; use std::collections::HashMap; use std::net::SocketAddrV4; use std::path::{Path, PathBuf}; use std::str::FromStr; -use std::sync::{mpsc, Mutex}; +use std::sync::{mpsc, Arc, Mutex}; use std::thread; use std::time::Duration; -use failure::{err_msg, Error}; +use failure::Error; use hyper::rt::Future; -use hyper::service::service_fn_ok; -use hyper::{Body, Request, Response, Server, StatusCode}; +use hyper::service::service_fn; +use hyper::Server; use notify::{RecommendedWatcher, RecursiveMode, Watcher}; use regex::Regex; -use walkdir::WalkDir; pub use config::Config; pub use handler::*; use hook::*; +use service::*; const URIPATTERN_STR: &str = r"/webhook/(?P[A-Za-z._][A-Za-z0-9._]*)"; lazy_static! { static ref URIPATTERN: Regex = Regex::new(URIPATTERN_STR).unwrap(); - static ref HANDLERS: Mutex>> = Mutex::new(HashMap::new()); + // static ref HANDLERS: Mutex>> = Mutex::new(HashMap::new()); static ref PROGRAMS: Mutex> = Mutex::new(HashMap::new()); - static ref HOOKS: Mutex> = Mutex::new(HashMap::new()); + static ref HOOKS: Arc>> = Arc::new(Mutex::new(HashMap::new())); } -const NOTFOUND: &str = "

Looks like you took a wrong turn!

There's nothing to see here.

"; - -fn service_fn(req: Request) -> Result, Error> { - let path = req.uri().path().to_owned(); - let captures = URIPATTERN - .captures(path.as_ref()) - .ok_or(err_msg("Did not match url pattern"))?; - let name = captures - .name("name") - .ok_or(err_msg("Missing name"))? - .as_str(); - let hooks = HOOKS.lock().unwrap(); - let hook = hooks - .get(name) - .ok_or(err_msg(format!("Hook '{}' doesn't exist", name)))?; - - let req_obj = { - let headers = req - .headers() - .clone() - .into_iter() - .filter_map(|(k, v)| { - let key = k.unwrap().as_str().to_owned(); - v.to_str().map(|value| (key, value.to_owned())).ok() - }).collect::>(); - let method = req.method().as_str().to_owned(); - // probably not idiomatically the best way to do it - // i was just trying to get something working - let body = "wip".to_owned(); - json!({ - "body": body, - "headers": headers, - "method": method, - }) - }; - hook.iter() - .fold(Ok(req_obj), |prev, handler| { - prev.and_then(|val| handler.run(val)) - }).map(|_| Response::new(Body::from("success"))) -} - -fn service_fn_wrapper(req: Request) -> Response { - let uri = req.uri().path().to_owned(); - service_fn(req).unwrap_or_else(|err| { - eprintln!("Error from '{}': {}", uri, err); - Response::builder() - .status(StatusCode::NOT_FOUND) - .body(Body::from(NOTFOUND)) - .unwrap() - }) -} - -fn load_config

(root: P) -where - P: AsRef, -{ - println!("Reloading config..."); - // hold on to the lock while config is being reloaded - { - let mut programs = PROGRAMS.lock().unwrap(); - // TODO: some kind of smart diff - programs.clear(); - let programs_dir = { - let mut p = root.as_ref().to_path_buf(); - p.push("handlers"); - p - }; - if programs_dir.exists() { - for entry in WalkDir::new(programs_dir) { - let path = match entry.as_ref().map(|e| e.path()) { - Ok(path) => path, - _ => continue, - }; - if !path.is_file() { - continue; - } - match path - .file_name() - .and_then(|s| s.to_str()) - .ok_or(err_msg("???")) - .map(|s| { - let filename = s.to_owned(); - programs.insert(filename, path.to_path_buf()) - }) { - _ => (), // don't care - } - } - } - } - { - let mut hooks = HOOKS.lock().unwrap(); - hooks.clear(); - let hooks_dir = { - let mut p = root.as_ref().to_path_buf(); - p.push("hooks"); - p - }; - if hooks_dir.exists() { - for entry in WalkDir::new(hooks_dir) { - let path = match entry.as_ref().map(|e| e.path()) { - Ok(path) => path, - _ => continue, - }; - if !path.is_file() { - continue; - } - match (|path: &Path| -> Result<(), Error> { - let hook = Hook::from_file(path)?; - let name = hook.get_name(); - hooks.insert(name, hook); - Ok(()) - })(path) - { - Ok(_) => (), - Err(err) => eprintln!("Failed to read config from {:?}: {}", path, err), - } - } - } - } -} +// const NOTFOUND: &str = "

Looks like you took a wrong turn!

There's nothing to see here.

"; fn watch

(root: P) -> notify::Result<()> where @@ -181,7 +66,7 @@ where Ok(_) => { // for now, naively reload entire config every time // TODO: don't do this - load_config(root.as_ref()) + config::load_config(root.as_ref()) } Err(e) => println!("watch error: {:?}", e), } @@ -190,14 +75,14 @@ where /// Main entry point of the entire application. pub fn run(config: &Config) -> Result<(), Error> { - load_config(&config.root); + config::load_config(&config.root); let v = config.root.clone(); thread::spawn(|| watch(v)); let addr: SocketAddrV4 = SocketAddrV4::from_str(config.bind.as_ref())?; let server = Server::bind(&addr.into()) - .serve(|| service_fn_ok(service_fn_wrapper)) + .serve(|| service_fn(dip_service)) .map_err(|e| eprintln!("server error: {}", e)); println!("Listening on {:?}", addr); hyper::rt::run(server); diff --git a/src/main.rs b/src/main.rs index 27290ba..88a3586 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,32 +1,12 @@ -#[macro_use] -extern crate structopt; extern crate dip; extern crate failure; - -use std::path::PathBuf; +extern crate structopt; use dip::Config; use failure::Error; use structopt::StructOpt; -#[derive(Debug, StructOpt)] -struct Opt { - /// The root configuration directory for dip. This argument is required. - #[structopt(short = "d", long = "root", parse(from_os_str))] - root: PathBuf, - /// A string containing the address to bind to. This defaults to "0.0.0.0:5000". - #[structopt(short = "b", long = "bind")] - bind: Option, - /// If a hook is specified here, it will be triggered manually exactly once and then the - /// program will exit rather than running as a server. - #[structopt(short = "h", long = "hook")] - hook: Option, -} - fn main() -> Result<(), Error> { - let opt = Opt::from_args(); - println!("{:?}", opt); - - let config = Config::new(opt.root).bind(opt.bind).hook(opt.hook); + let config = Config::from_args(); dip::run(&config) } diff --git a/src/service.rs b/src/service.rs new file mode 100644 index 0000000..8afaa00 --- /dev/null +++ b/src/service.rs @@ -0,0 +1,81 @@ +use std::collections::HashMap; +use std::thread; + +use futures::{future, Future, Stream}; +use hyper::{Body, Error, Request, Response, StatusCode}; +use mktemp::Temp; + +use {HOOKS, URIPATTERN}; + +pub fn dip_service(req: Request) -> Box, Error = Error> + Send> { + let path = req.uri().path().to_owned(); + let captures = match URIPATTERN.captures(path.as_ref()) { + Some(value) => value, + None => { + return Box::new(future::ok( + Response::builder() + .status(StatusCode::NOT_FOUND) + .body(Body::from("not found")) + .unwrap(), + )) + } + }; + let name = match captures.name("name") { + Some(value) => value.as_str().to_owned(), + None => { + return Box::new(future::ok( + Response::builder() + .status(StatusCode::NOT_FOUND) + .body(Body::from("not found")) + .unwrap(), + )) + } + }; + + // TODO: filter by method as well + + let headers = req.headers() + .clone() + .into_iter() + .filter_map(|(k, v)| { + let key = k.unwrap().as_str().to_owned(); + v.to_str().map(|value| (key, value.to_owned())).ok() + }) + .collect::>(); + let method = req.method().as_str().to_owned(); + + // spawn job + thread::spawn(move || { + req.into_body().concat2().map(move |body| { + let body = String::from_utf8(body.to_vec()).unwrap(); + let req_obj = json!({ + "body": body, + "headers": headers, + "method": method, + }); + let hooks = HOOKS.lock().unwrap(); + { + let mut temp_dir = Temp::new_dir().unwrap(); + let temp_path = temp_dir.to_path_buf(); + assert!(temp_path.exists()); + + let hook = hooks.get(&name).unwrap(); + let (code, msg) = hook.handle(req_obj, &temp_path).unwrap(); + + temp_dir.release(); + Response::builder() + .status(code) + .body(Body::from(msg)) + .unwrap_or_else(|err| Response::new(Body::from(format!("{}", err)))) + } + }) + }); + + Box::new(future::ok( + Response::builder() + .status(StatusCode::ACCEPTED) + // TODO: assign job a uuid and do some logging + .body(Body::from(format!("job {} started", -1))) + .unwrap(), + )) +}