Compare commits
15 commits
Author | SHA1 | Date | |
---|---|---|---|
faba2949d2 | |||
57d4f10b41 | |||
35b31b2a15 | |||
71153b28ec | |||
f6ec8af944 | |||
55c5134840 | |||
f38c70373c | |||
f25ee6b943 | |||
976c25ba1a | |||
81be79d46d | |||
f195162ce5 | |||
0312a600ed | |||
f7aea10c6b | |||
620fa520ce | |||
856cdc9457 |
9 changed files with 714 additions and 724 deletions
37
.woodpecker.yml
Normal file
37
.woodpecker.yml
Normal file
|
@ -0,0 +1,37 @@
|
|||
pipeline:
|
||||
fmt:
|
||||
group: default
|
||||
image: rust_full
|
||||
commands:
|
||||
- cargo fmt --all -- --check
|
||||
|
||||
clippy:
|
||||
group: default
|
||||
image: rust_full
|
||||
commands:
|
||||
- cargo clippy --all-features
|
||||
|
||||
test:
|
||||
group: default
|
||||
image: rust_full
|
||||
commands:
|
||||
- cargo test
|
||||
|
||||
build:
|
||||
group: default
|
||||
image: rust_full
|
||||
commands:
|
||||
- cargo build
|
||||
- cargo build --release
|
||||
|
||||
build-deb:
|
||||
group: default
|
||||
image: rust_full
|
||||
commands:
|
||||
- cargo deb
|
||||
|
||||
doc:
|
||||
group: default
|
||||
image: rust_full
|
||||
commands:
|
||||
- cargo doc
|
1267
Cargo.lock
generated
1267
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
27
Cargo.toml
27
Cargo.toml
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "webhookey"
|
||||
version = "0.1.5"
|
||||
version = "0.1.6"
|
||||
authors = ["finga <webhookey@onders.org>"]
|
||||
edition = "2021"
|
||||
license = "GPL-3.0-or-later"
|
||||
|
@ -11,24 +11,23 @@ description = "Trigger scripts via http(s) requests"
|
|||
tls = ["rocket/tls"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
clap = { version = "4.3", features = ["derive"] }
|
||||
dirs = "4.0"
|
||||
env_logger = "0.9"
|
||||
hex = "0.4"
|
||||
hmac = "0.12"
|
||||
ipnet = { version = "2.3", features = ["serde"] }
|
||||
log = "0.4"
|
||||
regex = "1.5"
|
||||
rocket = "0.5.0-rc.1"
|
||||
run_script = "0.9"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
serde_yaml = "0.8"
|
||||
serde_regex = "1.1"
|
||||
regex = "1.5"
|
||||
dirs = "4.0"
|
||||
anyhow = "1.0"
|
||||
log = "0.4"
|
||||
env_logger = "0.9"
|
||||
hmac = "0.11"
|
||||
sha2 = "0.9"
|
||||
hex = "0.4"
|
||||
ipnet = { version = "2.3", features = ["serde"] }
|
||||
serde_yaml = "0.8"
|
||||
sha2 = "0.10"
|
||||
thiserror = "1.0"
|
||||
run_script = "0.9"
|
||||
clap = "3.0.0-beta.5"
|
||||
base64 = "0.13"
|
||||
|
||||
[package.metadata.deb]
|
||||
extended-description = "Webhookey receives requests in form of a so called Webhook as for example sent by Gitea. Those requests are matched against configured filters, if a filter matches, values from the header and the body can be passed to scripts as parameters which are then executed subsequently."
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
# Webhookey
|
||||
![status-badge](https://ci.onders.org/api/badges/finga/webhookey/status.svg?branch=main)
|
||||
|
||||
Webhookey is a web server listening for
|
||||
[webhooks](https://en.wikipedia.org/wiki/Webhook) as for example sent
|
||||
by Gitea's webhooks. Further, Webhookey allows you to specify rules
|
||||
|
|
74
src/auth.rs
74
src/auth.rs
|
@ -1,74 +0,0 @@
|
|||
use crate::WebhookeyError;
|
||||
use base64;
|
||||
use rocket::{
|
||||
http::Status,
|
||||
outcome::Outcome,
|
||||
request::{self, FromRequest},
|
||||
Request,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::net::{IpAddr, Ipv4Addr};
|
||||
|
||||
fn decode_to_creds<T: Into<String>>(base64_encoded: T) -> Option<(String, String)> {
|
||||
let decoded_creds = match base64::decode(base64_encoded.into()) {
|
||||
Ok(cred_bytes) => String::from_utf8(cred_bytes).unwrap(),
|
||||
Err(_) => return None,
|
||||
};
|
||||
|
||||
if let Some((username, password)) = decoded_creds.split_once(":") {
|
||||
Some((username.to_string(), password.to_string()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct BasicAuth {
|
||||
username: String,
|
||||
password: String,
|
||||
}
|
||||
|
||||
impl BasicAuth {
|
||||
pub fn new<T: Into<String>>(auth_header: T) -> Option<Self> {
|
||||
let key = auth_header.into();
|
||||
|
||||
if key.len() < 7 || &key[..6] != "Basic " {
|
||||
return None;
|
||||
}
|
||||
|
||||
let (username, password) = decode_to_creds(&key[6..])?;
|
||||
Some(Self { username, password })
|
||||
}
|
||||
}
|
||||
|
||||
#[rocket::async_trait]
|
||||
impl<'r> FromRequest<'r> for BasicAuth {
|
||||
type Error = WebhookeyError;
|
||||
|
||||
async fn from_request(request: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
|
||||
let keys: Vec<_> = request.headers().get("Authorization").collect();
|
||||
|
||||
match keys.len() {
|
||||
0 => Outcome::Forward(()),
|
||||
1 => match BasicAuth::new(keys[0]) {
|
||||
Some(auth_header) => Outcome::Success(auth_header),
|
||||
None => Outcome::Failure((
|
||||
Status::Unauthorized,
|
||||
WebhookeyError::Unauthorized(
|
||||
request
|
||||
.client_ip()
|
||||
.unwrap_or(IpAddr::V4(Ipv4Addr::UNSPECIFIED)),
|
||||
),
|
||||
)),
|
||||
},
|
||||
_ => Outcome::Failure((
|
||||
Status::Unauthorized,
|
||||
WebhookeyError::Unauthorized(
|
||||
request
|
||||
.client_ip()
|
||||
.unwrap_or(IpAddr::V4(Ipv4Addr::UNSPECIFIED)),
|
||||
),
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
12
src/cli.rs
12
src/cli.rs
|
@ -1,4 +1,4 @@
|
|||
use clap::{crate_authors, crate_version, AppSettings, Parser};
|
||||
use clap::Parser;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
pub enum Command {
|
||||
|
@ -7,16 +7,10 @@ pub enum Command {
|
|||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[clap(
|
||||
version = crate_version!(),
|
||||
author = crate_authors!(", "),
|
||||
global_setting = AppSettings::InferSubcommands,
|
||||
global_setting = AppSettings::PropagateVersion,
|
||||
)]
|
||||
pub struct Opts {
|
||||
/// Provide a path to the configuration file
|
||||
#[clap(short, long, value_name = "FILE")]
|
||||
#[arg(short, long, value_name = "FILE")]
|
||||
pub config: Option<String>,
|
||||
#[clap(subcommand)]
|
||||
#[command(subcommand)]
|
||||
pub command: Option<Command>,
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::{auth::BasicAuth, filters::IpFilter, hooks::Hook};
|
||||
use crate::{filters::IpFilter, hooks::Hook};
|
||||
use anyhow::{bail, Result};
|
||||
use log::info;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
@ -15,7 +15,6 @@ pub struct MetricsConfig {
|
|||
#[serde(deny_unknown_fields)]
|
||||
pub struct Config {
|
||||
pub metrics: Option<MetricsConfig>,
|
||||
pub auth: Option<BasicAuth>,
|
||||
pub hooks: BTreeMap<String, Hook>,
|
||||
}
|
||||
|
||||
|
|
11
src/hooks.rs
11
src/hooks.rs
|
@ -1,10 +1,9 @@
|
|||
use crate::{
|
||||
auth::BasicAuth,
|
||||
filters::{FilterType, IpFilter},
|
||||
Config, Metrics, WebhookeyError,
|
||||
};
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use hmac::{Hmac, Mac, NewMac};
|
||||
use hmac::{Hmac, Mac};
|
||||
use log::{debug, error, info, trace, warn};
|
||||
use rocket::{
|
||||
data::{FromData, ToByteUnit},
|
||||
|
@ -45,7 +44,8 @@ fn validate_request(secret: &str, signature: &str, data: &[u8]) -> Result<()> {
|
|||
.map_err(|e| anyhow!("Could not create hasher with secret: {}", e))?;
|
||||
mac.update(data);
|
||||
let raw_signature = hex::decode(signature.as_bytes())?;
|
||||
mac.verify(&raw_signature).map_err(|e| anyhow!("{}", e))
|
||||
mac.verify_slice(&raw_signature)
|
||||
.map_err(|e| anyhow!("{}", e))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
|
@ -53,7 +53,6 @@ fn validate_request(secret: &str, signature: &str, data: &[u8]) -> Result<()> {
|
|||
pub struct Hook {
|
||||
command: String,
|
||||
signature: String,
|
||||
auth: Option<BasicAuth>,
|
||||
ip_filter: Option<IpFilter>,
|
||||
secrets: Vec<String>,
|
||||
filter: FilterType,
|
||||
|
@ -89,7 +88,7 @@ impl Hook {
|
|||
if let Some('}') = command_template.next() {
|
||||
let expr = token.trim().split(' ').collect::<Vec<&str>>();
|
||||
|
||||
let replaced = match expr.get(0) {
|
||||
let replaced = match expr.first() {
|
||||
Some(&"header") => get_header_field(
|
||||
headers,
|
||||
expr.get(1).ok_or_else(|| {
|
||||
|
@ -317,6 +316,8 @@ pub async fn receive_hook<'a>(
|
|||
.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
metrics.hooks_successful.fetch_add(1, Ordering::Relaxed);
|
||||
});
|
||||
|
||||
Status::Ok
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
mod auth;
|
||||
mod cli;
|
||||
mod config;
|
||||
mod filters;
|
||||
|
@ -24,9 +23,9 @@ pub enum WebhookeyError {
|
|||
#[error("Could not evaluate filter request")]
|
||||
InvalidFilter,
|
||||
#[error("IO Error")]
|
||||
Io(std::io::Error),
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("Serde Error")]
|
||||
Serde(serde_json::Error),
|
||||
Serde(#[from] serde_json::Error),
|
||||
}
|
||||
|
||||
pub fn get_string(data: &serde_json::Value) -> Result<String, WebhookeyError> {
|
||||
|
|
Loading…
Reference in a new issue