diff --git a/README.md b/README.md index 37dcf4f..9990ed7 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,20 @@ Whereas `` depends on the platform: ### Configuration parameters +#### Metrics +A metrics page can optionally enabled to query stats of the currently +running webhookey instance. Note that stats are lost between restarts +of webhookey as those are not stored persistently. + +Example: +```yaml +metrics: + enabled: true + ip_filter: + allow: + - 127.0.0.1/31 +``` + #### Hooks With `hooks` you can configure a sequence of hooks. A single hook consists of the following fields: diff --git a/config.yml b/config.yml index 4d57324..ae94496 100644 --- a/config.yml +++ b/config.yml @@ -1,4 +1,9 @@ --- +metrics: + enabled: true + ip_filter: + deny: + - 127.0.0.1 hooks: hook1: command: /usr/bin/local/script_xy.sh {{ /field2/foo }} asdfasdf diff --git a/src/main.rs b/src/main.rs index 2bfbf6f..8a870b1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -119,9 +119,17 @@ impl IpFilter { } } +#[derive(Debug, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +struct MetricsConfig { + enabled: bool, + ip_filter: Option, +} + #[derive(Debug, Deserialize, Serialize)] #[serde(deny_unknown_fields)] struct Config { + metrics: Option, hooks: BTreeMap, } @@ -432,6 +440,48 @@ fn get_config() -> Result { bail!("No configuration file found."); } +fn get_metrics(metrics: &WebhookeyMetrics) -> String { + format!( + r"# HELP webhookey_requests_received Number of requests received +# TYPE webhookey_requests_received gauge +webhookey_requests_received {} +# HELP webhookey_requests_invalid Number of invalid requests received +# TYPE webhookey_requests_invalid gauge +webhookey_requests_invalid {} +# HELP webhookey_hooks_successful Number of successfully executed hooks +# TYPE webhookey_hooks_successful gauge +webhookey_hooks_sucessful {} +# HELP webhookey_hooks_forbidden Number of forbidden requests +# TYPE webhookey_hooks_forbidden gauge +webhookey_hooks_forbidden {} +# HELP webhookey_hooks_unmatched Number of unmatched requests +# TYPE webhookey_hooks_unmatched gauge +webhookey_hooks_unmatched {} +# HELP webhookey_commands_executed Number of commands executed +# TYPE webhookey_commands_executed gauge +webhookey_commands_executed {} +# HELP webhookey_commands_execution_failed Number of commands failed to execute +# TYPE webhookey_commands_execution_failed gauge +webhookey_commands_execution_failed {} +# HELP webhookey_commands_successful Number of executed commands returning return code 0 +# TYPE webhookey_commands_successful gauge +webhookey_commands_successful {} +# HELP webhookey_commands_failed Number of executed commands returning different return code than 0 +# TYPE webhookey_commands_failed gauge +webhookey_commands_failed {} +", + metrics.requests_received.lock().unwrap(), // TODO: Check if unwrap need to be fixed + metrics.requests_invalid.lock().unwrap(), // TODO: Check if unwrap need to be fixed + metrics.hooks_successful.lock().unwrap(), // TODO: Check if unwrap need to be fixed + metrics.hooks_forbidden.lock().unwrap(), // TODO: Check if unwrap need to be fixed + metrics.hooks_unmatched.lock().unwrap(), // TODO: Check if unwrap need to be fixed + metrics.commands_executed.lock().unwrap(), // TODO: Check if unwrap need to be fixed + metrics.commands_execution_failed.lock().unwrap(), // TODO: Check if unwrap need to be fixed + metrics.commands_successful.lock().unwrap(), // TODO: Check if unwrap need to be fixed + metrics.commands_failed.lock().unwrap() // TODO: Check if unwrap need to be fixed + ) +} + #[rocket::async_trait] impl<'r> FromData<'r> for Hooks { type Error = WebhookeyError; @@ -554,46 +604,24 @@ async fn receive_hook<'a>( } #[get("/metrics")] -async fn metrics(metrics: &State) -> String { - format!( - r"# HELP webhookey_requests_received Number of requests received -# TYPE webhookey_requests_received gauge -webhookey_requests_received {:?} -# HELP webhookey_requests_invalid Number of invalid requests received -# TYPE webhookey_requests_invalid gauge -webhookey_requests_invalid {:?} -# HELP webhookey_hooks_successful Number of successfully executed hooks -# TYPE webhookey_hooks_successful gauge -webhookey_hooks_sucessful {:?} -# HELP webhookey_hooks_forbidden Number of forbidden requests -# TYPE webhookey_hooks_forbidden gauge -webhookey_hooks_forbidden {:?} -# HELP webhookey_hooks_unmatched Number of unmatched requests -# TYPE webhookey_hooks_unmatched gauge -webhookey_hooks_unmatched {:?} -# HELP webhookey_commands_executed Number of commands executed -# TYPE webhookey_commands_executed gauge -webhookey_commands_executed {:?} -# HELP webhookey_commands_execution_failed Number of commands failed to execute -# TYPE webhookey_commands_execution_failed gauge -webhookey_commands_execution_failed {:?} -# HELP webhookey_commands_successful Number of executed commands returning return code 0 -# TYPE webhookey_commands_successful gauge -webhookey_commands_successful {:?} -# HELP webhookey_commands_failed Number of executed commands returning different return code than 0 -# TYPE webhookey_commands_failed gauge -webhookey_commands_failed {:?} -", - metrics.requests_received.lock().unwrap(), // TODO: Check if unwrap need to be fixed - metrics.requests_invalid.lock().unwrap(), // TODO: Check if unwrap need to be fixed - metrics.hooks_successful.lock().unwrap(), // TODO: Check if unwrap need to be fixed - metrics.hooks_forbidden.lock().unwrap(), // TODO: Check if unwrap need to be fixed - metrics.hooks_unmatched.lock().unwrap(), // TODO: Check if unwrap need to be fixed - metrics.commands_executed.lock().unwrap(), // TODO: Check if unwrap need to be fixed - metrics.commands_execution_failed.lock().unwrap(), // TODO: Check if unwrap need to be fixed - metrics.commands_successful.lock().unwrap(), // TODO: Check if unwrap need to be fixed - metrics.commands_failed.lock().unwrap() // TODO: Check if unwrap need to be fixed - ) +async fn metrics( + address: SocketAddr, + metrics: &State, + config: &State, +) -> Option { + if let Some(metrics_config) = &config.metrics { + if let Some(filter) = &metrics_config.ip_filter { + if filter.validate(&address.ip()) { + return Some(get_metrics(&metrics)); + } + } else { + return Some(get_metrics(&metrics)); + } + } + + warn!("Forbidden request for metrics: {:?}", address); + + None } #[rocket::main] @@ -652,7 +680,10 @@ mod tests { }, ); - let config = Config { hooks: hooks }; + let config = Config { + metrics: None, + hooks: hooks, + }; let rocket = rocket::build() .mount("/", routes![receive_hook]) @@ -838,7 +869,7 @@ mod tests { ); let config = Config { - // default: None, + metrics: None, hooks: hooks, }; @@ -896,7 +927,7 @@ mod tests { ); let config = Config { - // default: None, + metrics: None, hooks: hooks, }; @@ -933,4 +964,133 @@ mod tests { assert_eq!(response.await.status(), Status::NotFound); } + + #[test] + fn parse_config() { + let config: Config = serde_yaml::from_str( + r#"--- +hooks: + hook1: + command: /usr/bin/local/script_xy.sh {{ /field2/foo }} asdfasdf + signature: X-Gitea-Signature + ip_filter: + allow: + - 127.0.0.1/31 + secrets: + - secret_key_01 + - secret_key_02 + filter: + json: + pointer: /ref + regex: refs/heads/master + hook2: + command: /usr/bin/local/script_xy.sh asdfasdf + signature: X-Gitea-Signature + secrets: + - secret_key_01 + - secret_key_02 + filter: + and: + - json: + pointer: /ref + regex: refs/heads/master + - json: + pointer: /after + regex: f6e5fe4fe37df76629112d55cc210718b6a55e7e"#, + ) + .unwrap(); + + assert_eq!( + serde_yaml::to_string(&config).unwrap(), + serde_yaml::to_string(&Config { + metrics: None, + hooks: BTreeMap::from([ + ( + "hook1".to_string(), + Hook { + command: "/usr/bin/local/script_xy.sh {{ /field2/foo }} asdfasdf" + .to_string(), + signature: "X-Gitea-Signature".to_string(), + ip_filter: Some(IpFilter::Allow(vec![AddrType::IpNet( + "127.0.0.1/31".parse().unwrap() + )])), + secrets: vec!["secret_key_01".to_string(), "secret_key_02".to_string()], + filter: FilterType::JsonFilter(JsonFilter { + pointer: "/ref".to_string(), + regex: "refs/heads/master".to_string(), + }), + } + ), + ( + "hook2".to_string(), + Hook { + command: "/usr/bin/local/script_xy.sh asdfasdf".to_string(), + signature: "X-Gitea-Signature".to_string(), + ip_filter: None, + secrets: vec!["secret_key_01".to_string(), "secret_key_02".to_string()], + filter: FilterType::And(vec![ + FilterType::JsonFilter(JsonFilter { + pointer: "/ref".to_string(), + regex: "refs/heads/master".to_string(), + }), + FilterType::JsonFilter(JsonFilter { + pointer: "/after".to_string(), + regex: "f6e5fe4fe37df76629112d55cc210718b6a55e7e".to_string(), + }), + ]), + } + ) + ]) + }) + .unwrap() + ); + + let config: Config = serde_yaml::from_str( + r#"--- +metrics: + enabled: true +hooks: + hook1: + command: /usr/bin/local/script_xy.sh {{ /field2/foo }} asdfasdf + signature: X-Gitea-Signature + ip_filter: + allow: + - 127.0.0.1/31 + secrets: + - secret_key_01 + - secret_key_02 + filter: + json: + pointer: /ref + regex: refs/heads/master"#, + ) + .unwrap(); + + assert_eq!( + serde_yaml::to_string(&config).unwrap(), + serde_yaml::to_string(&Config { + metrics: Some(MetricsConfig { + enabled: true, + ip_filter: None + }), + hooks: BTreeMap::from([( + "hook1".to_string(), + Hook { + command: "/usr/bin/local/script_xy.sh {{ /field2/foo }} asdfasdf" + .to_string(), + signature: "X-Gitea-Signature".to_string(), + ip_filter: Some(IpFilter::Allow(vec![AddrType::IpNet( + "127.0.0.1/31".parse().unwrap() + )])), + secrets: vec!["secret_key_01".to_string(), "secret_key_02".to_string()], + filter: FilterType::JsonFilter(JsonFilter { + pointer: "/ref".to_string(), + regex: "refs/heads/master".to_string(), + }), + } + ),]) + }) + .unwrap() + ); + } }