diff --git a/Cargo.lock b/Cargo.lock index 2b579a1..f5c47b4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -91,7 +91,7 @@ checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ "hermit-abi", "libc", - "winapi 0.3.9", + "winapi", ] [[package]] @@ -172,12 +172,6 @@ version = "1.0.67" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd" -[[package]] -name = "cfg-if" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" - [[package]] name = "cfg-if" version = "1.0.0" @@ -234,7 +228,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7e9d99fa91428effe99c5c6d4634cdeba32b8cf784fc428a2a687f61a952c49" dependencies = [ "autocfg", - "cfg-if 1.0.0", + "cfg-if", "lazy_static", ] @@ -315,7 +309,7 @@ checksum = "8e93d7f5705de3e49895a2b5e0b8855a1c27f080192ae9c32a6432d50741a57a" dependencies = [ "libc", "redox_users", - "winapi 0.3.9", + "winapi", ] [[package]] @@ -337,53 +331,6 @@ dependencies = [ "termcolor", ] -[[package]] -name = "filetime" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d34cfa13a63ae058bfa601fe9e313bbdb3746427c1459185464ce0fcf62e1e8" -dependencies = [ - "cfg-if 1.0.0", - "libc", - "redox_syscall 0.2.5", - "winapi 0.3.9", -] - -[[package]] -name = "fsevent" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ab7d1bd1bd33cc98b0889831b72da23c0aa4df9cec7e0702f46ecea04b35db6" -dependencies = [ - "bitflags", - "fsevent-sys", -] - -[[package]] -name = "fsevent-sys" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f41b048a94555da0f42f1d632e2e19510084fb8e303b0daa2816e733fb3644a0" -dependencies = [ - "libc", -] - -[[package]] -name = "fuchsia-zircon" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" -dependencies = [ - "bitflags", - "fuchsia-zircon-sys", -] - -[[package]] -name = "fuchsia-zircon-sys" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" - [[package]] name = "funty" version = "1.1.0" @@ -406,7 +353,7 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "libc", "wasi 0.9.0+wasi-snapshot-preview1", ] @@ -417,7 +364,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9495705279e7140bf035dde1f6e750c162df8b625267cd52cc44e0b156732c8" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "libc", "wasi 0.10.2+wasi-snapshot-preview1", ] @@ -453,6 +400,12 @@ dependencies = [ "libc", ] +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "hkdf" version = "0.10.0" @@ -537,51 +490,12 @@ dependencies = [ "hashbrown", ] -[[package]] -name = "inotify" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4816c66d2c8ae673df83366c18341538f234a26d65a9ecea5c348b453ac1d02f" -dependencies = [ - "bitflags", - "inotify-sys", - "libc", -] - -[[package]] -name = "inotify-sys" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" -dependencies = [ - "libc", -] - -[[package]] -name = "iovec" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" -dependencies = [ - "libc", -] - [[package]] name = "itoa" version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" -[[package]] -name = "kernel32-sys" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" -dependencies = [ - "winapi 0.2.8", - "winapi-build", -] - [[package]] name = "language-tags" version = "0.2.2" @@ -594,12 +508,6 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" -[[package]] -name = "lazycell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" - [[package]] name = "lexical-core" version = "0.7.5" @@ -608,7 +516,7 @@ checksum = "21f866863575d0e1d654fbeeabdc927292fdf862873dc3c96c6f753357e13374" dependencies = [ "arrayvec", "bitflags", - "cfg-if 1.0.0", + "cfg-if", "ryu", "static_assertions", ] @@ -640,7 +548,7 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", ] [[package]] @@ -664,60 +572,6 @@ dependencies = [ "log 0.3.9", ] -[[package]] -name = "mio" -version = "0.6.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4afd66f5b91bf2a3bc13fad0e21caedac168ca4c707504e75585648ae80e4cc4" -dependencies = [ - "cfg-if 0.1.10", - "fuchsia-zircon", - "fuchsia-zircon-sys", - "iovec", - "kernel32-sys", - "libc", - "log 0.4.14", - "miow", - "net2", - "slab", - "winapi 0.2.8", -] - -[[package]] -name = "mio-extras" -version = "2.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52403fe290012ce777c4626790c8951324a2b9e3316b3143779c72b029742f19" -dependencies = [ - "lazycell", - "log 0.4.14", - "mio", - "slab", -] - -[[package]] -name = "miow" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebd808424166322d4a38da87083bfddd3ac4c131334ed55856112eb06d46944d" -dependencies = [ - "kernel32-sys", - "net2", - "winapi 0.2.8", - "ws2_32-sys", -] - -[[package]] -name = "net2" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "391630d12b68002ae1e25e8f974306474966550ad82dac6886fb8910c19568ae" -dependencies = [ - "cfg-if 0.1.10", - "libc", - "winapi 0.3.9", -] - [[package]] name = "nom" version = "6.1.2" @@ -731,24 +585,6 @@ dependencies = [ "version_check 0.9.3", ] -[[package]] -name = "notify" -version = "4.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80ae4a7688d1fab81c5bf19c64fc8db920be8d519ce6336ed4e7efe024724dbd" -dependencies = [ - "bitflags", - "filetime", - "fsevent", - "fsevent-sys", - "inotify", - "libc", - "mio", - "mio-extras", - "walkdir", - "winapi 0.3.9", -] - [[package]] name = "num_cpus" version = "1.13.0" @@ -904,15 +740,6 @@ version = "0.1.57" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" -[[package]] -name = "redox_syscall" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94341e4e44e24f6b591b59e47a8a027df12e008d73fd5672dbea9cc22f4507d9" -dependencies = [ - "bitflags", -] - [[package]] name = "redox_users" version = "0.3.5" @@ -920,7 +747,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d" dependencies = [ "getrandom 0.1.16", - "redox_syscall 0.1.57", + "redox_syscall", "rust-argon2", ] @@ -989,19 +816,6 @@ dependencies = [ "yansi", ] -[[package]] -name = "rocket_contrib" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7954a707f9ca18aa74ca8c1f5d1f900f52a4dceb68e96e3112143f759cfd20e" -dependencies = [ - "log 0.4.14", - "notify", - "rocket", - "serde", - "serde_json", -] - [[package]] name = "rocket_http" version = "0.4.7" @@ -1059,15 +873,6 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - [[package]] name = "sct" version = "0.4.0" @@ -1128,18 +933,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa827a14b29ab7f44778d14a88d3cb76e949c45083f7dbfa507d0cb699dc12de" dependencies = [ "block-buffer", - "cfg-if 1.0.0", + "cfg-if", "cpuid-bool 0.1.2", "digest", "opaque-debug", ] -[[package]] -name = "slab" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" - [[package]] name = "smallvec" version = "1.6.1" @@ -1208,7 +1007,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" dependencies = [ "libc", - "winapi 0.3.9", + "winapi", ] [[package]] @@ -1331,17 +1130,6 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" -[[package]] -name = "walkdir" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "777182bc735b6424e1a57516d35ed72cb8019d85c8c9bf536dccb3445c1a2f7d" -dependencies = [ - "same-file", - "winapi 0.3.9", - "winapi-util", -] - [[package]] name = "wasi" version = "0.9.0+wasi-snapshot-preview1" @@ -1361,14 +1149,16 @@ dependencies = [ "anyhow", "dirs", "env_logger", + "hex", + "hmac", "log 0.4.14", "nom", "regex", "rocket", - "rocket_contrib", "serde", "serde_json", "serde_yaml", + "sha2", ] [[package]] @@ -1391,12 +1181,6 @@ dependencies = [ "webpki", ] -[[package]] -name = "winapi" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" - [[package]] name = "winapi" version = "0.3.9" @@ -1407,12 +1191,6 @@ dependencies = [ "winapi-x86_64-pc-windows-gnu", ] -[[package]] -name = "winapi-build" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" - [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" @@ -1425,7 +1203,7 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" dependencies = [ - "winapi 0.3.9", + "winapi", ] [[package]] @@ -1434,16 +1212,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "ws2_32-sys" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" -dependencies = [ - "winapi 0.2.8", - "winapi-build", -] - [[package]] name = "wyz" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index 4b2dbca..87d8d65 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,6 @@ tls = ["rocket/tls"] [dependencies] rocket = "0.4" -rocket_contrib = { version = "0.4", default-features = false, features = ["json"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde_yaml = "0.8" @@ -22,3 +21,6 @@ anyhow = "1.0" log = "0.4" env_logger = "0.8" nom = "6" +hmac = "0.10" +sha2 = "0.9" +hex = "0.4" diff --git a/README.md b/README.md index a5e811b..28834f9 100644 --- a/README.md +++ b/README.md @@ -55,11 +55,12 @@ Configuration syntax is YAML and has to be done in following order: Right now there is only the configuration parameter for hooks, here each hook has to be configured, It contains following fields: -- command: Optional string for a command to be executed when all - filters match. Pointers ([RFC - 6901](https://tools.ietf.org/html/rfc6901)) to JSON fields may be - used to be replaced with data from the JSON data with `{{ - /field/pointed/to }}` +- command: String for a command to be executed when all filters + match. Pointers ([RFC 6901](https://tools.ietf.org/html/rfc6901)) to + JSON fields may be used to be replaced with data from the JSON data + with `{{ /field/pointed/to }}`. Further `{{ event }}` and `{{ + signature }}` are valid variables as they contain the values from + the regarding header fields of the http request. - secrets: List of secrets. - filters: List of filters. @@ -81,7 +82,6 @@ Whereas `` depends on the platform: - Windows: `{FOLDERID_RoamingAppData}` # TODOs -## Use `lazy_static` or `once_cell` for compiled regexes ## Use `clap` to parse command line arguments ## Implement the functionality to reply to certain webhooks ## Configure rocket via config.yml diff --git a/src/main.rs b/src/main.rs index 33ae35b..0834874 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,8 @@ #![feature(proc_macro_hygiene, decl_macro)] use anyhow::{anyhow, bail, Result}; -use log::{debug, info, trace, warn}; +use hmac::{Hmac, Mac, NewMac}; +use log::{debug, error, info, trace, warn}; use nom::{ branch::alt, bytes::complete::{tag, take_until}, @@ -11,12 +12,24 @@ use nom::{ Finish, IResult, }; use regex::Regex; -use rocket::{fairing::AdHoc, get, http::Status, post, routes, Response, State}; -use rocket_contrib::json::Json; +use rocket::{ + data::{self, FromDataSimple}, + fairing::AdHoc, + get, + http::{HeaderMap, Status}, + post, routes, Data, + Outcome::{Failure, Success}, + Request, Response, State, +}; use serde::{Deserialize, Serialize}; +use sha2::Sha256; use std::{ - collections::HashMap, fs::File, io::BufReader, net::SocketAddr, process::Command, + collections::HashMap, + fs::File, + io::{BufReader, Read}, + net::SocketAddr, + process::Command, str::from_utf8, }; @@ -27,7 +40,7 @@ struct Config { #[derive(Debug, Deserialize, Serialize)] struct Hook { - command: Option, + command: String, secrets: Vec, filters: HashMap, } @@ -38,28 +51,28 @@ struct Filter { regex: String, } -#[derive(Debug, Deserialize, Serialize)] -struct Data(serde_json::Value); +#[derive(Debug)] +struct Hooks(HashMap>); -#[get("/")] -fn index() -> &'static str { - "Hello, webhookey!" -} - -fn replace_parameter(input: &str, data: &serde_json::Value) -> Result { +fn replace_parameter(input: &str, headers: &HeaderMap, data: &serde_json::Value) -> Result { let parse: IResult<&str, Vec<&str>> = many0(alt(( map_res( delimited(tag("{{"), take_until("}}"), tag("}}")), - |param: &str| { - if let Some(value) = data.pointer(param.trim()) { - if let Some(value) = value.as_str() { - Ok(value) + |param: &str| match param.trim() { + "event" => { + if let Some(event) = headers.get_one("X-Gitea-Event") { + Ok(event) } else { - bail!("Could not convert field `{}` to string", param.trim()); + bail!("Could not extract event parameter from header"); } - } else { - bail!("Could not find `{}` in received data", param.trim()); } + pointer => match data.pointer(pointer) { + Some(value) => match value.as_str() { + Some(value) => Ok(value), + _ => bail!("Could not convert value `{}` to string", value), + }, + _ => bail!("Could not convert field `{}` to string", param.trim()), + }, }, ), take_until("{{"), @@ -73,92 +86,224 @@ fn replace_parameter(input: &str, data: &serde_json::Value) -> Result { Ok(result.join("")) } -fn execute_hook(name: &str, hook: &Hook, data: &serde_json::Value) -> Result<()> { - debug!("Running hook `{}`", name); +impl FromDataSimple for Hooks { + type Error = anyhow::Error; - for (filter_name, filter) in hook.filters.iter() { - debug!("Matching filter `{}`", filter_name); + fn from_data(request: &Request, data: Data) -> data::Outcome { + let config = request.guard::>().unwrap(); // should never fail - if let Some(value) = data.pointer(&filter.pointer) { - let regex = Regex::new(&filter.regex)?; + let mut hooks = HashMap::new(); - if let Some(value) = value.as_str() { - if !regex.is_match(value) { - info!("Filter `{}` in hook `{}` did not match", filter_name, name); - return Ok(()); + if let Some(signature) = request.headers().get_one("X-Gitea-Signature") { + let mut data = data.open(); + let mut buffer = Vec::new(); + + match data.read_to_end(&mut buffer) { + Ok(_) => {} + Err(e) => { + error!("Could not read to end of data: {}", &e); + return Failure(( + Status::BadRequest, + anyhow!("Could not read to end of data: {}", &e), + )); } - } else { - anyhow!( - "Could not parse pointer in hook `{}` from filter `{}`", - name, - filter_name - ); } - } - } - if let Some(command) = &hook.command { - let command = replace_parameter(&command, data)?; + trace!("Data received: {:?}", from_utf8(&buffer)); - info!("Execute `{}` from hook `{}`", command, name); + let mut valid = false; - let command = command.split(' ').collect::>(); - let exec_command = Command::new(&command[0]).args(&command[1..]).output()?; + for (hook_name, hook) in &config.hooks { + let mut commands = Vec::new(); - info!( - "Command `{}` exited with return code: {}", - &command[0], &exec_command.status - ); - trace!( - "Output of command `{}` on stdout: {:?}", - &command[0], - from_utf8(&exec_command.stdout)? - ); - debug!( - "Output of command `{}` on stderr: {:?}", - &command[0], - from_utf8(&exec_command.stderr)? - ); - } + for secret in &hook.secrets { + let mut mac = match Hmac::::new_varkey(&secret.as_bytes()) { + Ok(mac) => mac, + Err(e) => { + error!("Could not instantiate hasher: {}", e); + return Failure(( + Status::InternalServerError, + anyhow!("Could not instantiate hasher: {}", e), + )); + } + }; - Ok(()) -} + mac.update(&buffer); -#[post("/", format = "json", data = "")] -fn receive_hook(address: SocketAddr, config: State, data: Json) -> Result { - info!("Post request received from: {}", address); + match &hex::decode(&signature.as_bytes()) { + Ok(raw_signature) => { + if mac.verify(&raw_signature) == Ok(()) { + trace!( + "Valid signature found for hook `{}`: {}", + hook_name, + signature + ); - let mut response = Response::new(); - let data = serde_json::to_value(data.0)?; + valid = true; - trace!("Data received from: {}\n{}", address, data); + let data: serde_json::Value = match serde_json::from_slice(&buffer) + { + Ok(data) => data, + Err(e) => { + error!("Could not parse json: {}", e); + return Failure(( + Status::BadRequest, + anyhow!("Could not parse json: {}", e), + )); + } + }; - if let Some(secret) = data.pointer("/secret") { - if let Some(secret) = secret.as_str() { - let hooks: HashMap<&String, &Hook> = config - .hooks - .iter() - .filter(|(_hook_name, hook)| hook.secrets.contains(&secret.to_string())) - .collect(); + for (filter_name, filter) in &hook.filters { + trace!( + "Matching filter `{}` of hook `{}`", + filter_name, + hook_name + ); + + let regex = match Regex::new(&filter.regex) { + Ok(regex) => regex, + Err(e) => { + error!( + "Could not compile regex `{}`: {}", + &filter.regex, e + ); + continue; + } + }; + + if let Some(value) = data.pointer(&filter.pointer) { + if let Some(value) = value.as_str() { + if regex.is_match(value) { + debug!( + "Filter `{}` of hook `{}` matched", + filter_name, hook_name + ); + + match replace_parameter( + &hook.command.to_string(), + &request.headers(), + &data, + ) { + Ok(command) => commands.push(command), + Err(e) => error!( + "Could not replace all parameter in hook `{}`: {}", + hook_name, e + ), + } + } + } else { + anyhow!( + "Could not parse pointer in hook `{}` from filter `{}`", + hook_name, + filter_name + ); + } + } + + trace!( + "Filter `{}` of hook `{}` did not match", + filter_name, + hook_name + ); + } + } + } + Err(e) => { + error!("Invalid configuration: {}", e); + return Failure(( + Status::InternalServerError, + anyhow!("Invalid configuration: {}", e), + )); + } + } + } + + if !commands.is_empty() { + hooks.insert(hook_name.to_string(), commands); + } + } if hooks.is_empty() { - warn!("Secret from {} did not match any hook", address); - response.set_status(Status::Unauthorized); - } else { - for (hook_name, hook) in hooks { - execute_hook(&hook_name, &hook, &data)?; + if valid { + warn!( + "Unmatched hook from {:?} with signature {:?}", + &request.client_ip(), + &request.headers().get_one("X-Gitea-Signature") + ); + Failure(( + Status::NotFound, + anyhow!( + "Unmatched hook from {:?} with signature {:?}", + &request.client_ip(), + &request.headers().get_one("X-Gitea-Signature") + ), + )) + } else { + warn!( + "Unauthorized request from {:?} with signature {:?}", + &request.client_ip(), + &request.headers().get_one("X-Gitea-Signature") + ); + Failure(( + Status::Unauthorized, + anyhow!( + "Unauthorized request from {:?} with signature {:?}", + &request.client_ip(), + &request.headers().get_one("X-Gitea-Signature") + ), + )) } + } else { + Success(Hooks(hooks)) } } else { - warn!("Data received from {} contains invalid data", address); - response.set_status(Status::BadRequest); + Failure(( + Status::BadRequest, + anyhow!("Could not extract signature from header"), + )) + } + } +} + +#[get("/")] +fn index() -> &'static str { + "Hello, webhookey!" +} + +#[post("/", format = "json", data = "")] +fn receive_hook<'a>(address: SocketAddr, hooks: Hooks) -> Result> { + info!("Post request received from: {}", address); + + for hook in hooks.0 { + for command in hook.1 { + info!("Execute `{}` from hook `{}`", &command, &hook.0); + + let command = command.split(' ').collect::>(); + match Command::new(&command[0]).args(&command[1..]).output() { + Ok(executed) => { + info!( + "Command `{}` exited with return code: {}", + &command[0], &executed.status + ); + trace!( + "Output of command `{}` on stdout: {:?}", + &command[0], + from_utf8(&executed.stdout)? + ); + debug!( + "Output of command `{}` on stderr: {:?}", + &command[0], + from_utf8(&executed.stderr)? + ); + } + Err(e) => { + error!("Execution of `{}` failed: {}", command[0], e); + } + } } - } else { - warn!("Data received from {} did not contain a secret", address); - response.set_status(Status::NotFound); } - Ok(response) + Ok(Response::new()) } fn get_config() -> Result { @@ -210,7 +355,10 @@ fn main() -> Result<()> { #[cfg(test)] mod tests { use super::*; - use rocket::{http::ContentType, local::Client}; + use rocket::{ + http::{ContentType, Header}, + local::Client, + }; use serde_json::json; #[test] @@ -230,7 +378,7 @@ mod tests { hooks.insert( "test_hook".to_string(), Hook { - command: None, + command: "".to_string(), secrets: vec!["valid".to_string()], filters: HashMap::new(), }, @@ -246,33 +394,33 @@ mod tests { let client = Client::new(rocket).unwrap(); let response = client .post("/") + .header(Header::new( + "X-Gitea-Signature", + "28175a0035f637f3cbb85afee9f9d319631580e7621cf790cd16ca063a2f820e", + )) .header(ContentType::JSON) .remote("127.0.0.1:8000".parse().unwrap()) - .body(r#"{ "secret": "valid" }"#) - .dispatch(); - - assert_eq!(response.status(), Status::Ok); - - let response = client - .post("/") - .header(ContentType::JSON) - .remote("127.0.0.1:8000".parse().unwrap()) - .body(r#"{ "secret": "invalid" }"#) - .dispatch(); - - assert_eq!(response.status(), Status::Unauthorized); - - let response = client - .post("/") - .header(ContentType::JSON) - .remote("127.0.0.1:8000".parse().unwrap()) - .body(r#"{ "not_secret": "invalid" }"#) + .body(&serde_json::to_string(&json!({ "foo": "bar" })).unwrap()) .dispatch(); assert_eq!(response.status(), Status::NotFound); let response = client .post("/") + .header(Header::new("X-Gitea-Signature", "beef")) + .header(ContentType::JSON) + .remote("127.0.0.1:8000".parse().unwrap()) + .body(&serde_json::to_string(&json!({ "foo": "bar" })).unwrap()) + .dispatch(); + + assert_eq!(response.status(), Status::Unauthorized); + + let response = client + .post("/") + .header(Header::new( + "X-Gitea-Signature", + "c5c315d76318362ec129ca629b50b626bba09ad3d7ba4cc0f4c0afe4a90537a0", + )) .header(ContentType::JSON) .remote("127.0.0.1:8000".parse().unwrap()) .body(r#"{ "not_secret": "invalid" "#) @@ -283,44 +431,48 @@ mod tests { #[test] fn parse_command() { + let mut map = HeaderMap::new(); + map.add_raw("X-Gitea-Event", "something"); + assert_eq!( - replace_parameter("command", &serde_json::Value::Null).unwrap(), + replace_parameter("command", &map, &serde_json::Value::Null).unwrap(), "command" ); assert_eq!( - replace_parameter(" command", &serde_json::Value::Null).unwrap(), + replace_parameter(" command", &map, &serde_json::Value::Null).unwrap(), " command" ); assert_eq!( - replace_parameter("command ", &serde_json::Value::Null).unwrap(), + replace_parameter("command ", &map, &serde_json::Value::Null).unwrap(), "command " ); assert_eq!( - replace_parameter(" command ", &serde_json::Value::Null).unwrap(), + replace_parameter(" command ", &map, &serde_json::Value::Null).unwrap(), " command " ); assert_eq!( - replace_parameter("command command ", &serde_json::Value::Null).unwrap(), + replace_parameter("command command ", &map, &serde_json::Value::Null).unwrap(), "command command " ); assert_eq!( - replace_parameter("{{ /foo }} command", &json!({ "foo": "bar" })).unwrap(), + replace_parameter("{{ /foo }} command", &map, &json!({ "foo": "bar" })).unwrap(), "bar command" ); assert_eq!( - replace_parameter(" command {{ /foo }} ", &json!({ "foo": "bar" })).unwrap(), + replace_parameter(" command {{ /foo }} ", &map, &json!({ "foo": "bar" })).unwrap(), " command bar " ); assert_eq!( replace_parameter( "{{ /foo }} command{{/field1/foo}}", + &map, &json!({ "foo": "bar", "field1": { "foo": "baz" } }) ) .unwrap(), @@ -328,17 +480,20 @@ mod tests { ); assert_eq!( - replace_parameter(" command {{ /foo }} ", &json!({ "foo": "bar" })).unwrap(), + replace_parameter(" command {{ /foo }} ", &map, &json!({ "foo": "bar" })).unwrap(), " command bar " ); assert_eq!( replace_parameter( " {{ /field1/foo }} command", + &map, &json!({ "field1": { "foo": "bar" } }) ) .unwrap(), " bar command" ); + + // Add tests with header fields } }