Merge branch 'master' of github:acmumn/dip
This commit is contained in:
commit
9bc9a41cd5
10 changed files with 410 additions and 218 deletions
37
Cargo.lock
generated
37
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
12
README.md
12
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
|
||||
-------
|
||||
|
|
|
@ -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<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> {
|
||||
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::<Sha1>::new(&secret);
|
||||
mac.input(payload.body.as_bytes());
|
||||
let signature = mac
|
||||
.result()
|
||||
.code()
|
||||
.into_iter()
|
||||
.map(|b| format!("{:02x}", b))
|
||||
.collect::<Vec<_>>()
|
||||
.join("");
|
||||
if !config.disable_hmac_verify {
|
||||
let secret = GenericArray::from_iter(config.secret.bytes());
|
||||
let mut mac = Hmac::<Sha1>::new(&secret);
|
||||
mac.input(payload.body.as_bytes());
|
||||
let signature = mac.result()
|
||||
.code()
|
||||
.into_iter()
|
||||
.map(|b| format!("{:02x}", b))
|
||||
.collect::<Vec<_>>()
|
||||
.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::<Vec<_>>());
|
||||
assert!(left == right, "HMAC signature didn't match");
|
||||
let left = SecStr::from(format!("sha1={}", signature));
|
||||
let right = SecStr::from(auth.bytes().collect::<Vec<_>>());
|
||||
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(())
|
||||
}
|
||||
|
|
|
@ -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<String>,
|
||||
}
|
||||
|
||||
|
@ -26,3 +40,72 @@ impl Config {
|
|||
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.");
|
||||
}
|
||||
|
|
135
src/handler.rs
135
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<Self, Error> {
|
||||
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<JsonValue, Error> {
|
||||
|
||||
pub fn run(&self, temp_path: &PathBuf, input: JsonValue) -> Result<JsonValue, Error> {
|
||||
let config = {
|
||||
let mut buf: Vec<u8> = 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,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
|
35
src/hook.rs
35
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<Path>,
|
||||
{
|
||||
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<Handler> {
|
||||
self.handlers.iter()
|
||||
}
|
||||
pub fn handle(&self, _payload: &Request<Body>) -> Result<Response<Body>, 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))))
|
||||
}
|
||||
}
|
||||
|
|
147
src/lib.rs
147
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<name>[A-Za-z._][A-Za-z0-9._]*)";
|
||||
|
||||
lazy_static! {
|
||||
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 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>";
|
||||
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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 watch<P>(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);
|
||||
|
|
24
src/main.rs
24
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<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> {
|
||||
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)
|
||||
}
|
||||
|
|
81
src/service.rs
Normal file
81
src/service.rs
Normal 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(),
|
||||
))
|
||||
}
|
Loading…
Reference in a new issue