Merge branch 'master' of github:acmumn/dip

This commit is contained in:
Michael Zhang 2018-08-16 20:21:57 -07:00
commit 9bc9a41cd5
No known key found for this signature in database
GPG key ID: A1B65B603268116B
10 changed files with 410 additions and 218 deletions

37
Cargo.lock generated
View file

@ -192,6 +192,7 @@ dependencies = [
"hmac 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)", "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)", "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)", "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)", "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)", "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)", "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)", "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]] [[package]]
name = "net2" name = "net2"
version = "0.2.33" version = "0.2.33"
@ -569,6 +578,16 @@ dependencies = [
"proc-macro2 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", "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]] [[package]]
name = "rand" name = "rand"
version = "0.4.2" version = "0.4.2"
@ -617,6 +636,11 @@ name = "rustc-demangle"
version = "0.1.9" version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" 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]] [[package]]
name = "rustc_version" name = "rustc_version"
version = "0.2.3" version = "0.2.3"
@ -960,6 +984,15 @@ name = "utf8-ranges"
version = "1.0.0" version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" 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]] [[package]]
name = "vec_map" name = "vec_map"
version = "0.8.1" 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 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.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 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 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 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" "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 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 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 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 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_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 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 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 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-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 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 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" "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 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 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 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 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 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" "checksum void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"

View file

@ -16,6 +16,7 @@ sha-1 = "0.7"
failure = "0.1" failure = "0.1"
futures = "0.1" futures = "0.1"
hyper = "0.12" hyper = "0.12"
mktemp = "0.3"
lazy_static = "1.1" lazy_static = "1.1"
notify = "4.0" notify = "4.0"
regex = "1.0" regex = "1.0"

View file

@ -6,7 +6,17 @@ Dip
Configurable webhook server. 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 Contact
------- -------

View file

@ -11,9 +11,11 @@ extern crate generic_array;
extern crate structopt; extern crate structopt;
use std::collections::HashMap; use std::collections::HashMap;
use std::env;
use std::io::{self, Read}; use std::io::{self, Read};
use std::iter::FromIterator; use std::iter::FromIterator;
use std::path::PathBuf; use std::path::PathBuf;
use std::process::Command;
use failure::{err_msg, Error}; use failure::{err_msg, Error};
use generic_array::GenericArray; use generic_array::GenericArray;
@ -29,10 +31,13 @@ struct Opt {
pub config: String, pub config: String,
} }
#[derive(Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
struct Config { struct Config {
secret: String, secret: String,
outdir: PathBuf, #[serde(default)]
disable_hmac_verify: bool,
#[serde(default = "default_path")]
path: PathBuf,
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
@ -41,36 +46,60 @@ struct Payload {
headers: HashMap<String, String>, headers: HashMap<String, String>,
} }
#[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> { fn main() -> Result<(), Error> {
let args = Opt::from_args(); let args = Opt::from_args();
let config: Config = serde_json::from_str(&args.config)?; let config: Config = serde_json::from_str(&args.config)?;
println!("{:?}", config);
let mut payload = String::new(); let mut payload = String::new();
io::stdin().read_to_string(&mut payload)?; io::stdin().read_to_string(&mut payload)?;
println!("raw payload: {}", payload);
let payload: Payload = serde_json::from_str(&payload)?; let payload: Payload = serde_json::from_str(&payload)?;
println!("processed payload: {}", payload.body);
let secret = GenericArray::from_iter(config.secret.bytes()); if !config.disable_hmac_verify {
let mut mac = Hmac::<Sha1>::new(&secret); let secret = GenericArray::from_iter(config.secret.bytes());
mac.input(payload.body.as_bytes()); let mut mac = Hmac::<Sha1>::new(&secret);
let signature = mac mac.input(payload.body.as_bytes());
.result() let signature = mac.result()
.code() .code()
.into_iter() .into_iter()
.map(|b| format!("{:02x}", b)) .map(|b| format!("{:02x}", b))
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(""); .join("");
let auth = payload let auth = payload
.headers .headers
.get("x-hub-signature") .get("x-hub-signature")
.ok_or(err_msg("Missing auth header"))?; .ok_or(err_msg("Missing auth header"))?;
let left = SecStr::from(format!("sha1={}", signature)); let left = SecStr::from(format!("sha1={}", signature));
let right = SecStr::from(auth.bytes().collect::<Vec<_>>()); let right = SecStr::from(auth.bytes().collect::<Vec<_>>());
assert!(left == right, "HMAC signature didn't match"); 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(()) Ok(())
} }

View file

@ -1,8 +1,22 @@
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use failure::{err_msg, Error};
use walkdir::WalkDir;
use Hook;
use {HOOKS, PROGRAMS};
#[derive(Debug, StructOpt)]
pub struct Config { 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, 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, 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<String>, pub hook: Option<String>,
} }
@ -26,3 +40,72 @@ impl Config {
return self; return self;
} }
} }
pub fn load_config<P>(root: P)
where
P: AsRef<Path>,
{
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.");
}

View file

@ -11,27 +11,52 @@ use PROGRAMS;
pub struct Handler { pub struct Handler {
config: TomlValue, config: TomlValue,
exec: PathBuf, action: Action,
}
pub enum Action {
Command(String),
Exec(PathBuf),
} }
impl Handler { impl Handler {
pub fn config(&self) -> &TomlValue {
&self.config
}
pub fn from(config: &TomlValue) -> Result<Self, Error> { pub fn from(config: &TomlValue) -> Result<Self, Error> {
let handler = config let handler = config
.get("type") .get("type")
.ok_or(err_msg("No 'type' found."))? .ok_or(err_msg("No 'type' found."))?
.as_str() .as_str()
.ok_or(err_msg("'type' is not a string."))?; .ok_or(err_msg("'type' is not a string."))?;
let exec = { let action = match handler {
let programs = PROGRAMS.lock().unwrap(); "command" => {
programs let command = config
.get(handler) .get("command")
.ok_or(err_msg(format!("'{}' is not a valid executable", handler))) .ok_or(err_msg("No 'command' found"))?
.map(|value| value.clone())? .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(); let config = config.clone();
Ok(Handler { config, exec }) Ok(Handler { config, action })
} }
pub fn run(&self, input: JsonValue) -> Result<JsonValue, Error> {
pub fn run(&self, temp_path: &PathBuf, input: JsonValue) -> Result<JsonValue, Error> {
let config = { let config = {
let mut buf: Vec<u8> = Vec::new(); let mut buf: Vec<u8> = Vec::new();
{ {
@ -41,33 +66,71 @@ impl Handler {
String::from_utf8(buf).unwrap() String::from_utf8(buf).unwrap()
}; };
let mut child = Command::new(&self.exec) let output = match &self.action {
.env("DIP_ROOT", "") Action::Command(ref cmd) => {
.arg("--config") // TODO: allow some kind of simple variable replacement
.arg(config) let child = Command::new("/bin/bash")
.stdin(Stdio::piped()) .current_dir(&temp_path)
.stdout(Stdio::piped()) .env("DIP_ROOT", "lol")
.stderr(Stdio::piped()) .env("DIP_WORKDIR", temp_path)
.spawn()?; .arg("-c")
{ .arg(cmd)
match child.stdin { .stdin(Stdio::piped())
Some(ref mut stdin) => { .stdout(Stdio::piped())
write!(stdin, "{}", input)?; .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"), output
}; }
} Action::Exec(ref path) => {
let output = child.wait_with_output()?; let mut child = Command::new(&path)
if !output.status.success() { .current_dir(&temp_path)
// TODO: get rid of unwraps .env("DIP_ROOT", "")
return Err(err_msg(format!( .env("DIP_WORKDIR", temp_path)
"'{:?}' returned with a non-zero status code: {}\nstdout:\n{}\nstderr:\n{}", .arg("--config")
self.exec, .arg(config)
output.status, .stdin(Stdio::piped())
String::from_utf8(output.stdout).unwrap(), .stdout(Stdio::piped())
String::from_utf8(output.stderr).unwrap() .stderr(Stdio::piped())
))); .spawn()?;
} {
Ok(json!({})) 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,
}))
} }
} }

View file

@ -1,10 +1,11 @@
use std::fs::File; use std::fs::File;
use std::io::Read; use std::io::Read;
use std::path::Path; use std::path::{Path, PathBuf};
use std::slice::Iter; use std::slice::Iter;
use failure::{err_msg, Error}; use failure::{err_msg, Error};
use hyper::{Body, Request, Response}; use hyper::StatusCode;
use serde_json::Value as JsonValue;
use toml::Value; use toml::Value;
use Handler; use Handler;
@ -31,8 +32,7 @@ impl Hook {
where where
P: AsRef<Path>, P: AsRef<Path>,
{ {
let filename = path let filename = path.as_ref()
.as_ref()
.file_name() .file_name()
.ok_or(err_msg("what the fuck bro"))? .ok_or(err_msg("what the fuck bro"))?
.to_str() .to_str()
@ -52,7 +52,30 @@ impl Hook {
pub fn iter(&self) -> Iter<Handler> { pub fn iter(&self) -> Iter<Handler> {
self.handlers.iter() self.handlers.iter()
} }
pub fn handle(&self, _payload: &Request<Body>) -> Result<Response<Body>, Error> { pub fn handle(
Ok(Response::new(Body::from("lol"))) &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))))
} }
} }

View file

@ -2,14 +2,17 @@
#[macro_use] #[macro_use]
extern crate failure; extern crate failure;
extern crate hyper;
extern crate futures; extern crate futures;
extern crate hyper;
extern crate mktemp;
extern crate serde; extern crate serde;
#[macro_use] #[macro_use]
extern crate serde_json; extern crate serde_json;
#[macro_use] #[macro_use]
extern crate lazy_static; extern crate lazy_static;
extern crate notify; extern crate notify;
#[macro_use]
extern crate structopt;
extern crate regex; extern crate regex;
extern crate toml; extern crate toml;
extern crate walkdir; extern crate walkdir;
@ -17,156 +20,38 @@ extern crate walkdir;
pub mod config; pub mod config;
pub mod handler; pub mod handler;
pub mod hook; pub mod hook;
pub mod service;
use std::collections::HashMap; use std::collections::HashMap;
use std::net::SocketAddrV4; use std::net::SocketAddrV4;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::str::FromStr; use std::str::FromStr;
use std::sync::{mpsc, Mutex}; use std::sync::{mpsc, Arc, Mutex};
use std::thread; use std::thread;
use std::time::Duration; use std::time::Duration;
use failure::{err_msg, Error}; use failure::Error;
use hyper::rt::Future; use hyper::rt::Future;
use hyper::service::service_fn_ok; use hyper::service::service_fn;
use hyper::{Body, Request, Response, Server, StatusCode}; use hyper::Server;
use notify::{RecommendedWatcher, RecursiveMode, Watcher}; use notify::{RecommendedWatcher, RecursiveMode, Watcher};
use regex::Regex; use regex::Regex;
use walkdir::WalkDir;
pub use config::Config; pub use config::Config;
pub use handler::*; pub use handler::*;
use hook::*; use hook::*;
use service::*;
const URIPATTERN_STR: &str = r"/webhook/(?P<name>[A-Za-z._][A-Za-z0-9._]*)"; const URIPATTERN_STR: &str = r"/webhook/(?P<name>[A-Za-z._][A-Za-z0-9._]*)";
lazy_static! { lazy_static! {
static ref URIPATTERN: Regex = Regex::new(URIPATTERN_STR).unwrap(); static ref URIPATTERN: Regex = Regex::new(URIPATTERN_STR).unwrap();
static ref HANDLERS: Mutex<HashMap<String, Box<Handler>>> = Mutex::new(HashMap::new()); // static ref HANDLERS: Mutex<HashMap<String, Box<Handler>>> = Mutex::new(HashMap::new());
static ref PROGRAMS: Mutex<HashMap<String, PathBuf>> = Mutex::new(HashMap::new()); static ref PROGRAMS: Mutex<HashMap<String, PathBuf>> = Mutex::new(HashMap::new());
static ref HOOKS: Mutex<HashMap<String, Hook>> = Mutex::new(HashMap::new()); static ref HOOKS: Arc<Mutex<HashMap<String, Hook>>> = Arc::new(Mutex::new(HashMap::new()));
} }
const NOTFOUND: &str = "<html> <head> <style> * { font-family: sans-serif; } body { padding: 20px 60px; } </style> </head> <body> <h1>Looks like you took a wrong turn!</h1> <p>There's nothing to see here.</p> </body> </html>"; // const NOTFOUND: &str = "<html> <head> <style> * { font-family: sans-serif; } body { padding: 20px 60px; } </style> </head> <body> <h1>Looks like you took a wrong turn!</h1> <p>There's nothing to see here.</p> </body> </html>";
fn service_fn(req: Request<Body>) -> Result<Response<Body>, 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::<HashMap<_, _>>();
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<Body>) -> Response<Body> {
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<P>(root: P)
where
P: AsRef<Path>,
{
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),
}
}
}
}
}
fn watch<P>(root: P) -> notify::Result<()> fn watch<P>(root: P) -> notify::Result<()>
where where
@ -181,7 +66,7 @@ where
Ok(_) => { Ok(_) => {
// for now, naively reload entire config every time // for now, naively reload entire config every time
// TODO: don't do this // TODO: don't do this
load_config(root.as_ref()) config::load_config(root.as_ref())
} }
Err(e) => println!("watch error: {:?}", e), Err(e) => println!("watch error: {:?}", e),
} }
@ -190,14 +75,14 @@ where
/// Main entry point of the entire application. /// Main entry point of the entire application.
pub fn run(config: &Config) -> Result<(), Error> { pub fn run(config: &Config) -> Result<(), Error> {
load_config(&config.root); config::load_config(&config.root);
let v = config.root.clone(); let v = config.root.clone();
thread::spawn(|| watch(v)); thread::spawn(|| watch(v));
let addr: SocketAddrV4 = SocketAddrV4::from_str(config.bind.as_ref())?; let addr: SocketAddrV4 = SocketAddrV4::from_str(config.bind.as_ref())?;
let server = Server::bind(&addr.into()) 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)); .map_err(|e| eprintln!("server error: {}", e));
println!("Listening on {:?}", addr); println!("Listening on {:?}", addr);
hyper::rt::run(server); hyper::rt::run(server);

View file

@ -1,32 +1,12 @@
#[macro_use]
extern crate structopt;
extern crate dip; extern crate dip;
extern crate failure; extern crate failure;
extern crate structopt;
use std::path::PathBuf;
use dip::Config; use dip::Config;
use failure::Error; use failure::Error;
use structopt::StructOpt; 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<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")]
hook: Option<String>,
}
fn main() -> Result<(), Error> { fn main() -> Result<(), Error> {
let opt = Opt::from_args(); let config = Config::from_args();
println!("{:?}", opt);
let config = Config::new(opt.root).bind(opt.bind).hook(opt.hook);
dip::run(&config) dip::run(&config)
} }

81
src/service.rs Normal file
View file

@ -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<Body>) -> Box<Future<Item = Response<Body>, 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::<HashMap<_, _>>();
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(),
))
}