diff --git a/README.md b/README.md index 28834f9..3f08801 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,7 @@ each hook has to be configured, It contains following fields: 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. +- signature: Name of the HTTP header field containing the signature. - secrets: List of secrets. - filters: List of filters. diff --git a/src/main.rs b/src/main.rs index c985766..309745e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -41,6 +41,7 @@ struct Config { #[derive(Debug, Deserialize, Serialize)] struct Hook { command: String, + signature: String, secrets: Vec, filters: HashMap, } @@ -52,7 +53,7 @@ struct Filter { } #[derive(Debug)] -struct Hooks(HashMap>); +struct Hooks(HashMap); fn validate_request(secret: &str, signature: &str, data: &[u8]) -> Result<()> { let mut mac = Hmac::::new_varkey(&secret.as_bytes()) @@ -118,7 +119,7 @@ fn filter_match( )?)); } } else { - anyhow!( + bail!( "Could not parse pointer in hook `{}` from filter `{}`", hook_name, filter_name @@ -139,40 +140,28 @@ impl FromDataSimple for Hooks { type Error = anyhow::Error; fn from_data(request: &Request, data: Data) -> data::Outcome { - let config = request.guard::>().unwrap(); // should never fail + let mut buffer = Vec::new(); + match data.open().read_to_end(&mut buffer) { + Ok(size) => info!("Data of size {} received", size), + Err(e) => { + error!("Could not read to end of data: {}", &e); + return Failure(( + Status::BadRequest, + anyhow!("Could not read to end of data: {}", &e), + )); + } + } + let config = request.guard::>().unwrap(); // should never fail + let mut valid = false; let mut hooks = HashMap::new(); - 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), - )); - } - } - - trace!("Data received: {:?}", from_utf8(&buffer)); - - let mut valid = false; - - for (hook_name, hook) in &config.hooks { - let mut commands = Vec::new(); - + for (hook_name, hook) in &config.hooks { + if let Some(signature) = request.headers().get_one(&hook.signature) { for secret in &hook.secrets { match validate_request(&secret, &signature, &buffer) { Ok(()) => { - trace!( - "Valid signature found for hook `{}`: {}", - hook_name, - signature - ); + trace!("Valid signature found for hook `{}`", hook_name,); valid = true; @@ -196,7 +185,10 @@ impl FromDataSimple for Hooks { &request, &data, ) { - Ok(Some(command)) => commands.push(command), + Ok(Some(command)) => { + hooks.insert(hook_name.to_string(), command); + break; + } Ok(None) => {} Err(e) => error!("{}", e), } @@ -211,51 +203,32 @@ impl FromDataSimple for Hooks { } } } - - if !commands.is_empty() { - hooks.insert(hook_name.to_string(), commands); - } - } - - if hooks.is_empty() { - 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)) + error!("Could not extract signature from header"); + return Failure(( + Status::BadRequest, + anyhow!("Could not extract signature from header"), + )); } - } else { - Failure(( - Status::BadRequest, - anyhow!("Could not extract signature from header"), - )) } + + if hooks.is_empty() { + if valid { + warn!("Unmatched hook from {:?}", &request.client_ip()); + return Failure(( + Status::NotFound, + anyhow!("Unmatched hook from {:?}", &request.client_ip()), + )); + } else { + error!("Unauthorized request from {:?}", &request.client_ip()); + return Failure(( + Status::Unauthorized, + anyhow!("Unauthorized request from {:?}", &request.client_ip()), + )); + } + } + + Success(Hooks(hooks)) } } @@ -269,30 +242,28 @@ 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); + info!("Execute `{}` from hook `{}`", &hook.1, &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); - } + let command = hook.1.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); } } } @@ -373,6 +344,7 @@ mod tests { "test_hook".to_string(), Hook { command: "".to_string(), + signature: "X-Gitea-Signature".to_string(), secrets: vec!["valid".to_string()], filters: HashMap::new(), }, @@ -497,6 +469,14 @@ mod tests { " bar command" ); - // Add tests with header fields + assert_eq!( + replace_parameter( + " {{ event }} command", + &map, + &json!({ "field1": { "foo": "bar" } }) + ) + .unwrap(), + " something command" + ); } }