Compare commits
11 Commits
12eed6b967
...
52b5598c97
Author | SHA1 | Date |
---|---|---|
trivernis | 52b5598c97 | 1 year ago |
trivernis | b59867d832 | 1 year ago |
trivernis | 491a8d1a69 | 1 year ago |
trivernis | bef104ead5 | 1 year ago |
trivernis | 74661986f2 | 1 year ago |
trivernis | fa5ee3fa37 | 1 year ago |
trivernis | 5829d8ca35 | 1 year ago |
trivernis | 36af236bdb | 1 year ago |
trivernis | 198f0b65ce | 1 year ago |
trivernis | dbddc5879c | 1 year ago |
trivernis | cd4011014d | 1 year ago |
@ -1 +1,2 @@
|
|||||||
/target
|
/target
|
||||||
|
.multihook.toml
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,28 +0,0 @@
|
|||||||
use crate::secret_validation::SecretValidator;
|
|
||||||
use hmac::{Hmac, Mac, NewMac};
|
|
||||||
use hyper::HeaderMap;
|
|
||||||
use sha2::Sha256;
|
|
||||||
|
|
||||||
pub struct GithubSecretValidator;
|
|
||||||
|
|
||||||
static X_HUB_SIGNATURE_256_HEADER: &str = "X-Hub-Signature-256";
|
|
||||||
|
|
||||||
impl SecretValidator for GithubSecretValidator {
|
|
||||||
fn validate(&self, headers: &HeaderMap, body: &[u8], secret: &[u8]) -> bool {
|
|
||||||
log::debug!("Validating GitHub Secret");
|
|
||||||
if let Some(github_sum) = headers.get(X_HUB_SIGNATURE_256_HEADER) {
|
|
||||||
let mut mac = Hmac::<Sha256>::new_from_slice(secret).unwrap();
|
|
||||||
mac.update(body);
|
|
||||||
|
|
||||||
let decoded_secret = if let Ok(decoded) = hex::decode(github_sum) {
|
|
||||||
decoded
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
mac.verify(&decoded_secret).is_ok()
|
|
||||||
} else {
|
|
||||||
log::debug!("Missing Signature Header");
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,43 @@
|
|||||||
|
use crate::secret_validation::SecretValidator;
|
||||||
|
use hmac::{Hmac, Mac};
|
||||||
|
use hyper::HeaderMap;
|
||||||
|
use sha2::Sha256;
|
||||||
|
|
||||||
|
pub struct HMacSecretValidator;
|
||||||
|
|
||||||
|
static SUM_HEADERS: &[&str] = &[
|
||||||
|
"X-Forgejo-Signature",
|
||||||
|
"X-Gitea-Signature",
|
||||||
|
"X-Gogs-Signature",
|
||||||
|
"X-Hub-Signature-256",
|
||||||
|
];
|
||||||
|
|
||||||
|
impl SecretValidator for HMacSecretValidator {
|
||||||
|
fn validate(&self, headers: &HeaderMap, body: &[u8], secret: &[u8]) -> bool {
|
||||||
|
log::debug!("Validating HMac Secret");
|
||||||
|
let header = headers
|
||||||
|
.iter()
|
||||||
|
.filter(|(name, _)| SUM_HEADERS.iter().find(|h| **name == **h).is_some())
|
||||||
|
.next();
|
||||||
|
|
||||||
|
if let Some((_, sum)) = header {
|
||||||
|
let mut mac = Hmac::<Sha256>::new_from_slice(secret).unwrap();
|
||||||
|
mac.update(body);
|
||||||
|
let Ok(sum) = sum.to_str() else {
|
||||||
|
log::error!("Received signature is not a valid string");
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Ok(decoded_secret) = hex::decode(sum.trim_start_matches("sha256=")) else {
|
||||||
|
log::error!("Received signature cannot be decoded from hex");
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
log::debug!("Verifying found signature");
|
||||||
|
|
||||||
|
mac.verify_slice(&decoded_secret).is_ok()
|
||||||
|
} else {
|
||||||
|
log::error!("Missing Signature Header");
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,118 +0,0 @@
|
|||||||
use crate::secret_validation::SecretValidator;
|
|
||||||
use crate::server::command_template::CommandTemplate;
|
|
||||||
use crate::utils::error::{MultihookError, MultihookResult};
|
|
||||||
use crate::utils::settings::{EndpointSettings, SecretSettings};
|
|
||||||
use hyper::http::request::Parts;
|
|
||||||
use hyper::{Body, Request};
|
|
||||||
use serde_json::Value;
|
|
||||||
use std::fs::read_to_string;
|
|
||||||
use std::mem;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use tokio::process::Command;
|
|
||||||
use tokio::sync::Semaphore;
|
|
||||||
|
|
||||||
static MAX_CONCURRENCY: usize = 256;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct HookAction {
|
|
||||||
command: CommandTemplate,
|
|
||||||
parallel_lock: Arc<Semaphore>,
|
|
||||||
run_detached: bool,
|
|
||||||
secret: Option<SecretSettings>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl HookAction {
|
|
||||||
pub fn new<S: ToString>(
|
|
||||||
command: S,
|
|
||||||
parallel: bool,
|
|
||||||
detached: bool,
|
|
||||||
secret: Option<SecretSettings>,
|
|
||||||
) -> Self {
|
|
||||||
let parallel_lock = if parallel {
|
|
||||||
Semaphore::new(MAX_CONCURRENCY)
|
|
||||||
} else {
|
|
||||||
Semaphore::new(1)
|
|
||||||
};
|
|
||||||
Self {
|
|
||||||
command: CommandTemplate::new(command),
|
|
||||||
parallel_lock: Arc::new(parallel_lock),
|
|
||||||
run_detached: detached,
|
|
||||||
secret,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn execute(&self, req: Request<Body>) -> MultihookResult<()> {
|
|
||||||
let (parts, body) = req.into_parts();
|
|
||||||
let body = hyper::body::to_bytes(body).await?.to_vec();
|
|
||||||
|
|
||||||
self.validate_secret(&parts, &body)?;
|
|
||||||
let body = String::from_utf8(body)?;
|
|
||||||
|
|
||||||
if self.run_detached {
|
|
||||||
tokio::spawn({
|
|
||||||
let action = self.clone();
|
|
||||||
async move {
|
|
||||||
if let Err(e) = action.execute_command(&body).await {
|
|
||||||
log::error!("Detached hook threw an error: {:?}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
self.execute_command(&body).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn validate_secret(&self, parts: &Parts, body: &Vec<u8>) -> MultihookResult<()> {
|
|
||||||
if let Some(secret) = &self.secret {
|
|
||||||
let validator = secret.format.validator();
|
|
||||||
if !validator.validate(&parts.headers, &body, &secret.value.as_bytes()) {
|
|
||||||
return Err(MultihookError::InvalidSecret);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn execute_command(&self, body: &str) -> MultihookResult<()> {
|
|
||||||
let json_body: Value = serde_json::from_str(body).unwrap_or_default();
|
|
||||||
let command = self.command.evaluate(&json_body);
|
|
||||||
log::debug!("Acquiring lock for parallel runs...");
|
|
||||||
let permit = self.parallel_lock.acquire().await.unwrap();
|
|
||||||
log::debug!("Lock acquired. Running command...");
|
|
||||||
|
|
||||||
let output = Command::new("sh")
|
|
||||||
.env("HOOK_BODY", body)
|
|
||||||
.arg("-c")
|
|
||||||
.arg(command)
|
|
||||||
.kill_on_drop(true)
|
|
||||||
.output()
|
|
||||||
.await?;
|
|
||||||
log::debug!("Command finished. Releasing parallel lock...");
|
|
||||||
mem::drop(permit);
|
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr[..]);
|
|
||||||
let stdout = String::from_utf8_lossy(&output.stdout[..]);
|
|
||||||
log::debug!("Command output is: {}", stdout);
|
|
||||||
|
|
||||||
if stderr.len() > 0 {
|
|
||||||
log::error!("Errors occurred during command execution: {}", stderr);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&EndpointSettings> for HookAction {
|
|
||||||
fn from(endpoint: &EndpointSettings) -> Self {
|
|
||||||
let action = endpoint.action.clone();
|
|
||||||
let path = PathBuf::from(&action);
|
|
||||||
let contents = read_to_string(path).unwrap_or(action);
|
|
||||||
Self::new(
|
|
||||||
contents,
|
|
||||||
endpoint.allow_parallel,
|
|
||||||
endpoint.run_detached,
|
|
||||||
endpoint.secret.clone(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,64 @@
|
|||||||
|
use crate::utils::error::{MultihookError, MultihookResult};
|
||||||
|
|
||||||
|
use self::template::ActionTemplate;
|
||||||
|
use std::{collections::HashMap, sync::Arc};
|
||||||
|
use tokio::{process::Command, sync::Semaphore};
|
||||||
|
|
||||||
|
mod template;
|
||||||
|
|
||||||
|
static MAX_CONCURRENCY: usize = 256;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Action {
|
||||||
|
template: ActionTemplate,
|
||||||
|
semaphore: Arc<Semaphore>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Action {
|
||||||
|
/// Creates a new command that also checks for parallel runs
|
||||||
|
pub fn new<S: Into<String>>(command: S, allow_parallel: bool) -> Self {
|
||||||
|
let semaphore = if allow_parallel {
|
||||||
|
Semaphore::new(MAX_CONCURRENCY)
|
||||||
|
} else {
|
||||||
|
Semaphore::new(1)
|
||||||
|
};
|
||||||
|
|
||||||
|
Self {
|
||||||
|
template: ActionTemplate::new(command.into()),
|
||||||
|
semaphore: Arc::new(semaphore),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Executes the action
|
||||||
|
pub async fn run(
|
||||||
|
&self,
|
||||||
|
body: &serde_json::Value,
|
||||||
|
env: &HashMap<&str, String>,
|
||||||
|
) -> MultihookResult<()> {
|
||||||
|
let command = self.template.evaluate(&body);
|
||||||
|
log::debug!("Acquiring lock for parallel runs...");
|
||||||
|
let permit = self.semaphore.acquire().await.unwrap();
|
||||||
|
log::debug!("Lock acquired. Running command...");
|
||||||
|
std::mem::drop(permit);
|
||||||
|
|
||||||
|
let output = Command::new("sh")
|
||||||
|
.envs(env)
|
||||||
|
.arg("-c")
|
||||||
|
.arg(command)
|
||||||
|
.kill_on_drop(true)
|
||||||
|
.output()
|
||||||
|
.await?;
|
||||||
|
log::debug!("Command finished. Releasing parallel lock...");
|
||||||
|
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr[..]);
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout[..]);
|
||||||
|
log::debug!("Command output is: {}", stdout);
|
||||||
|
|
||||||
|
if output.status.success() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
log::error!("Errors occurred during command execution: {}", stderr);
|
||||||
|
Err(MultihookError::ActionError(stderr.into_owned()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,159 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use crate::secret_validation::SecretValidator;
|
||||||
|
use crate::utils::error::{LogErr, MultihookError, MultihookResult};
|
||||||
|
use crate::utils::settings::{EndpointSettings, SecretSettings, Settings};
|
||||||
|
use hyper::http::request::Parts;
|
||||||
|
use hyper::{Body, Request};
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
use super::action::Action;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct HookEndpoint {
|
||||||
|
name: String,
|
||||||
|
action: Action,
|
||||||
|
global_hooks: ActionHooks,
|
||||||
|
hooks: ActionHooks,
|
||||||
|
run_detached: bool,
|
||||||
|
secret: Option<SecretSettings>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Default)]
|
||||||
|
struct ActionHooks {
|
||||||
|
pre: Option<Action>,
|
||||||
|
post: Option<Action>,
|
||||||
|
error: Option<Action>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HookEndpoint {
|
||||||
|
pub fn from_config<S: Into<String>>(
|
||||||
|
name: S,
|
||||||
|
global: &Settings,
|
||||||
|
endpoint: &EndpointSettings,
|
||||||
|
) -> Self {
|
||||||
|
let global_hooks = global
|
||||||
|
.hooks
|
||||||
|
.as_ref()
|
||||||
|
.map(|hooks_cfg| ActionHooks {
|
||||||
|
pre: hooks_cfg.pre_action.as_ref().map(|a| Action::new(a, true)),
|
||||||
|
post: hooks_cfg.post_action.as_ref().map(|a| Action::new(a, true)),
|
||||||
|
error: hooks_cfg.err_action.as_ref().map(|a| Action::new(a, true)),
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let hooks = endpoint
|
||||||
|
.hooks
|
||||||
|
.as_ref()
|
||||||
|
.map(|hooks_cfg| ActionHooks {
|
||||||
|
pre: hooks_cfg
|
||||||
|
.pre_action
|
||||||
|
.as_ref()
|
||||||
|
.map(|a| Action::new(a, endpoint.allow_parallel)),
|
||||||
|
post: hooks_cfg
|
||||||
|
.post_action
|
||||||
|
.as_ref()
|
||||||
|
.map(|a| Action::new(a, endpoint.allow_parallel)),
|
||||||
|
error: hooks_cfg
|
||||||
|
.err_action
|
||||||
|
.as_ref()
|
||||||
|
.map(|a| Action::new(a, endpoint.allow_parallel)),
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
name: name.into(),
|
||||||
|
action: Action::new(&endpoint.action, endpoint.allow_parallel),
|
||||||
|
run_detached: endpoint.run_detached,
|
||||||
|
secret: endpoint.secret.clone(),
|
||||||
|
global_hooks,
|
||||||
|
hooks,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn execute(&self, req: Request<Body>) -> MultihookResult<()> {
|
||||||
|
let (parts, body) = req.into_parts();
|
||||||
|
let body = hyper::body::to_bytes(body).await?.to_vec();
|
||||||
|
|
||||||
|
self.validate_secret(&parts, &body)?;
|
||||||
|
let body = String::from_utf8(body)?;
|
||||||
|
|
||||||
|
if self.run_detached {
|
||||||
|
tokio::spawn({
|
||||||
|
let action = self.clone();
|
||||||
|
async move {
|
||||||
|
if let Err(e) = action.execute_command(&body).await {
|
||||||
|
log::error!("Detached hook threw an error: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
self.execute_command(&body).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_secret(&self, parts: &Parts, body: &Vec<u8>) -> MultihookResult<()> {
|
||||||
|
if let Some(secret) = &self.secret {
|
||||||
|
let validator = secret.format.validator();
|
||||||
|
if !validator.validate(&parts.headers, &body, &secret.value.as_bytes()) {
|
||||||
|
return Err(MultihookError::InvalidSecret);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute_command(&self, body: &str) -> MultihookResult<()> {
|
||||||
|
let json_body: Value = serde_json::from_str(body).unwrap_or_default();
|
||||||
|
let mut env = HashMap::new();
|
||||||
|
env.insert("HOOK_NAME", self.name.to_owned());
|
||||||
|
env.insert("HOOK_BODY", body.to_string());
|
||||||
|
|
||||||
|
if let Some(global_pre) = &self.global_hooks.pre {
|
||||||
|
global_pre
|
||||||
|
.run(&json_body, &env)
|
||||||
|
.await
|
||||||
|
.log_err("Global Pre-Hook failed {e}");
|
||||||
|
}
|
||||||
|
if let Some(pre_hook) = &self.hooks.pre {
|
||||||
|
pre_hook
|
||||||
|
.run(&json_body, &env)
|
||||||
|
.await
|
||||||
|
.log_err("Endpoint Pre-Hook failed {e}");
|
||||||
|
}
|
||||||
|
if let Err(e) = self.action.run(&json_body, &env).await {
|
||||||
|
env.insert("HOOK_ERROR", format!("{e}"));
|
||||||
|
|
||||||
|
if let Some(global_err_action) = &self.global_hooks.error {
|
||||||
|
global_err_action
|
||||||
|
.run(&json_body, &env)
|
||||||
|
.await
|
||||||
|
.log_err("Global Error-Hook failed {e}");
|
||||||
|
}
|
||||||
|
if let Some(err_hook) = &self.hooks.error {
|
||||||
|
err_hook
|
||||||
|
.run(&json_body, &env)
|
||||||
|
.await
|
||||||
|
.log_err("Endpoint Error-Hook failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(e)
|
||||||
|
} else {
|
||||||
|
if let Some(global_post_hook) = &self.global_hooks.post {
|
||||||
|
global_post_hook
|
||||||
|
.run(&json_body, &env)
|
||||||
|
.await
|
||||||
|
.log_err("Global Post-Hook failed");
|
||||||
|
}
|
||||||
|
if let Some(post_hook) = &self.hooks.post {
|
||||||
|
post_hook
|
||||||
|
.run(&json_body, &env)
|
||||||
|
.await
|
||||||
|
.log_err("Endpoint Post-Hook failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue