2021-02-02 11:05:50 +01:00
|
|
|
#![feature(proc_macro_hygiene, decl_macro)]
|
|
|
|
|
2021-03-03 15:24:46 +01:00
|
|
|
use anyhow::{anyhow, bail, Result};
|
2021-06-30 23:55:21 +02:00
|
|
|
use clap::{crate_authors, crate_version, AppSettings, Clap};
|
2021-03-28 03:50:52 +02:00
|
|
|
use hmac::{Hmac, Mac, NewMac};
|
2021-04-02 00:25:39 +02:00
|
|
|
use ipnet::IpNet;
|
2021-03-28 03:50:52 +02:00
|
|
|
use log::{debug, error, info, trace, warn};
|
2021-03-21 15:51:58 +01:00
|
|
|
use nom::{
|
|
|
|
branch::alt,
|
|
|
|
bytes::complete::{tag, take_until},
|
|
|
|
combinator::map_res,
|
|
|
|
multi::many0,
|
|
|
|
sequence::delimited,
|
|
|
|
Finish, IResult,
|
|
|
|
};
|
2021-03-03 15:24:46 +01:00
|
|
|
use regex::Regex;
|
2021-03-28 03:50:52 +02:00
|
|
|
use rocket::{
|
|
|
|
data::{self, FromDataSimple},
|
|
|
|
fairing::AdHoc,
|
|
|
|
http::{HeaderMap, Status},
|
|
|
|
post, routes, Data,
|
|
|
|
Outcome::{Failure, Success},
|
|
|
|
Request, Response, State,
|
|
|
|
};
|
2021-04-16 17:42:40 +02:00
|
|
|
use run_script::ScriptOptions;
|
2021-02-02 11:05:50 +01:00
|
|
|
use serde::{Deserialize, Serialize};
|
2021-03-28 03:50:52 +02:00
|
|
|
use sha2::Sha256;
|
2021-04-03 01:10:50 +02:00
|
|
|
use thiserror::Error;
|
2021-02-02 11:05:50 +01:00
|
|
|
|
2021-03-22 11:12:45 +01:00
|
|
|
use std::{
|
2021-03-28 03:50:52 +02:00
|
|
|
collections::HashMap,
|
|
|
|
fs::File,
|
|
|
|
io::{BufReader, Read},
|
2021-04-02 00:25:39 +02:00
|
|
|
net::{IpAddr, Ipv4Addr, SocketAddr},
|
2021-03-22 11:12:45 +01:00
|
|
|
};
|
2021-03-03 15:24:46 +01:00
|
|
|
|
2021-06-02 03:35:43 +02:00
|
|
|
#[derive(Debug, Error)]
|
|
|
|
enum WebhookeyError {
|
|
|
|
#[error("Could not extract signature from header")]
|
|
|
|
InvalidSignature,
|
|
|
|
#[error("Unauthorized request from `{0}`")]
|
|
|
|
Unauthorized(IpAddr),
|
|
|
|
#[error("Unmatched hook from `{0}`")]
|
|
|
|
UnmatchedHook(IpAddr),
|
|
|
|
#[error("Could not find field refered to in parameter `{0}`")]
|
|
|
|
InvalidParameterPointer(String),
|
|
|
|
#[error("Could not evaluate filter request")]
|
|
|
|
InvalidFilter,
|
|
|
|
#[error("IO Error")]
|
|
|
|
Io(std::io::Error),
|
|
|
|
#[error("Serde Error")]
|
|
|
|
Serde(serde_json::Error),
|
|
|
|
#[error("Regex Error")]
|
|
|
|
Regex(regex::Error),
|
|
|
|
}
|
|
|
|
|
2021-06-30 23:55:21 +02:00
|
|
|
#[derive(Clap, Debug)]
|
|
|
|
enum Command {
|
|
|
|
/// Verifies if the configuration can be parsed without errors
|
|
|
|
Configtest,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Clap, Debug)]
|
|
|
|
#[clap(
|
|
|
|
version = crate_version!(),
|
|
|
|
author = crate_authors!(", "),
|
|
|
|
global_setting = AppSettings::VersionlessSubcommands,
|
|
|
|
global_setting = AppSettings::InferSubcommands,
|
|
|
|
)]
|
|
|
|
struct Opts {
|
|
|
|
/// Provide a path to the configuration file
|
|
|
|
#[clap(short, long, value_name = "FILE")]
|
|
|
|
config: Option<String>,
|
|
|
|
#[clap(subcommand)]
|
|
|
|
command: Option<Command>,
|
|
|
|
}
|
|
|
|
|
2021-03-03 15:24:46 +01:00
|
|
|
#[derive(Debug, Deserialize, Serialize)]
|
2021-04-02 00:25:39 +02:00
|
|
|
#[serde(deny_unknown_fields, untagged)]
|
|
|
|
enum AddrType {
|
|
|
|
IpAddr(IpAddr),
|
|
|
|
IpNet(IpNet),
|
|
|
|
}
|
|
|
|
|
2021-05-29 00:50:48 +02:00
|
|
|
impl AddrType {
|
|
|
|
fn matches(&self, client_ip: &IpAddr) -> bool {
|
|
|
|
match self {
|
|
|
|
AddrType::IpAddr(addr) => addr == client_ip,
|
|
|
|
AddrType::IpNet(net) => net.contains(client_ip),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-02 00:25:39 +02:00
|
|
|
#[derive(Debug, Deserialize, Serialize)]
|
|
|
|
#[serde(deny_unknown_fields, rename_all = "lowercase")]
|
|
|
|
enum IpFilter {
|
|
|
|
Allow(Vec<AddrType>),
|
|
|
|
Deny(Vec<AddrType>),
|
|
|
|
}
|
|
|
|
|
2021-05-29 00:50:48 +02:00
|
|
|
impl IpFilter {
|
|
|
|
fn validate(&self, client_ip: &IpAddr) -> bool {
|
|
|
|
match self {
|
|
|
|
IpFilter::Allow(list) => list.iter().any(|i| i.matches(client_ip)),
|
|
|
|
IpFilter::Deny(list) => !list.iter().any(|i| i.matches(client_ip)),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-06-30 23:55:21 +02:00
|
|
|
#[derive(Debug, Deserialize, Serialize)]
|
|
|
|
#[serde(deny_unknown_fields)]
|
|
|
|
struct Config {
|
|
|
|
hooks: HashMap<String, Hook>,
|
|
|
|
}
|
|
|
|
|
2021-05-31 02:16:22 +02:00
|
|
|
#[derive(Debug, Deserialize, Serialize)]
|
|
|
|
#[serde(deny_unknown_fields)]
|
|
|
|
struct JsonFilter {
|
|
|
|
pointer: String,
|
|
|
|
regex: String,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl JsonFilter {
|
|
|
|
fn evaluate(&self, data: &serde_json::Value) -> Result<bool, WebhookeyError> {
|
|
|
|
trace!(
|
|
|
|
"Matching `{}` on `{}` from received json",
|
|
|
|
&self.regex,
|
|
|
|
&self.pointer,
|
|
|
|
);
|
|
|
|
|
|
|
|
let regex = Regex::new(&self.regex).map_err(WebhookeyError::Regex)?;
|
|
|
|
|
|
|
|
if let Some(value) = data.pointer(&self.pointer) {
|
|
|
|
let value = get_string(value)?;
|
|
|
|
|
|
|
|
if regex.is_match(&value) {
|
|
|
|
debug!("Regex `{}` for `{}` matches", &self.regex, &self.pointer);
|
|
|
|
|
|
|
|
return Ok(true);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
debug!(
|
|
|
|
"Regex `{}` for `{}` does not match",
|
|
|
|
&self.regex, &self.pointer
|
|
|
|
);
|
|
|
|
|
|
|
|
Ok(false)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, Deserialize, Serialize)]
|
|
|
|
#[serde(deny_unknown_fields, rename_all = "lowercase")]
|
|
|
|
enum FilterType {
|
|
|
|
And(Vec<FilterType>),
|
|
|
|
Or(Vec<FilterType>),
|
|
|
|
#[serde(rename = "json")]
|
|
|
|
JsonFilter(JsonFilter),
|
|
|
|
}
|
|
|
|
|
|
|
|
impl FilterType {
|
|
|
|
fn evaluate(
|
|
|
|
&self,
|
|
|
|
request: &Request,
|
|
|
|
data: &serde_json::Value,
|
|
|
|
) -> Result<bool, WebhookeyError> {
|
|
|
|
match self {
|
|
|
|
FilterType::And(filters) => {
|
|
|
|
let (results, errors): (Vec<_>, Vec<_>) = filters
|
|
|
|
.iter()
|
|
|
|
.map(|filter| filter.evaluate(request, data))
|
|
|
|
.partition(Result::is_ok);
|
|
|
|
|
|
|
|
if errors.is_empty() {
|
|
|
|
Ok(results.iter().all(|r| *r.as_ref().unwrap())) // should never fail
|
|
|
|
} else {
|
|
|
|
errors.iter().for_each(|e| {
|
|
|
|
error!("Could not evaluate Filter: {}", e.as_ref().unwrap_err())
|
|
|
|
});
|
|
|
|
|
|
|
|
Err(WebhookeyError::InvalidFilter)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
FilterType::Or(filters) => {
|
|
|
|
let (results, errors): (Vec<_>, Vec<_>) = filters
|
|
|
|
.iter()
|
|
|
|
.map(|filter| filter.evaluate(request, data))
|
|
|
|
.partition(Result::is_ok);
|
|
|
|
|
|
|
|
if errors.is_empty() {
|
|
|
|
Ok(results.iter().any(|r| *r.as_ref().unwrap())) // should never fail
|
|
|
|
} else {
|
|
|
|
errors.iter().for_each(|e| {
|
|
|
|
error!("Could not evaluate Filter: {}", e.as_ref().unwrap_err())
|
|
|
|
});
|
|
|
|
|
|
|
|
Err(WebhookeyError::InvalidFilter)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
FilterType::JsonFilter(filter) => filter.evaluate(data),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-02 11:05:50 +01:00
|
|
|
#[derive(Debug, Deserialize, Serialize)]
|
2021-04-02 00:25:39 +02:00
|
|
|
#[serde(deny_unknown_fields)]
|
2021-03-03 15:24:46 +01:00
|
|
|
struct Hook {
|
2021-03-28 03:50:52 +02:00
|
|
|
command: String,
|
2021-03-29 04:21:31 +02:00
|
|
|
signature: String,
|
2021-04-02 00:25:39 +02:00
|
|
|
ip_filter: Option<IpFilter>,
|
2021-03-19 10:16:46 +01:00
|
|
|
secrets: Vec<String>,
|
2021-05-31 02:16:22 +02:00
|
|
|
filter: FilterType,
|
2021-03-03 15:24:46 +01:00
|
|
|
}
|
|
|
|
|
2021-06-02 03:35:43 +02:00
|
|
|
impl Hook {
|
|
|
|
fn get_command(
|
|
|
|
&self,
|
|
|
|
hook_name: &str,
|
|
|
|
request: &Request,
|
|
|
|
data: &mut serde_json::Value,
|
|
|
|
) -> Result<String> {
|
|
|
|
trace!("Replacing parameters for command of hook `{}`", hook_name);
|
|
|
|
|
|
|
|
for parameter in get_parameter(&self.command)? {
|
|
|
|
let parameter = parameter.trim();
|
|
|
|
|
|
|
|
if let Some(json_value) = data.pointer(parameter) {
|
|
|
|
*data.pointer_mut(parameter).ok_or_else(|| {
|
|
|
|
WebhookeyError::InvalidParameterPointer(parameter.to_string())
|
|
|
|
})? = serde_json::Value::String(get_string(json_value)?);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-06-30 23:52:19 +02:00
|
|
|
replace_parameters(&self.command, request.headers(), data)
|
2021-06-02 03:35:43 +02:00
|
|
|
}
|
2021-04-03 01:10:50 +02:00
|
|
|
}
|
|
|
|
|
2021-03-28 03:50:52 +02:00
|
|
|
#[derive(Debug)]
|
2021-05-29 00:50:48 +02:00
|
|
|
struct Hooks {
|
|
|
|
inner: HashMap<String, String>,
|
|
|
|
}
|
2021-02-02 11:05:50 +01:00
|
|
|
|
2021-06-02 03:35:43 +02:00
|
|
|
impl Hooks {
|
|
|
|
fn get_commands(request: &Request, data: Data) -> Result<Self, WebhookeyError> {
|
|
|
|
let mut buffer = Vec::new();
|
|
|
|
let size = data
|
|
|
|
.open()
|
|
|
|
.read_to_end(&mut buffer)
|
|
|
|
.map_err(WebhookeyError::Io)?;
|
|
|
|
info!("Data of size {} received", size);
|
|
|
|
|
|
|
|
let config = request.guard::<State<Config>>().unwrap(); // should never fail
|
|
|
|
let mut valid = false;
|
|
|
|
let mut result = HashMap::new();
|
|
|
|
let client_ip = &request
|
|
|
|
.client_ip()
|
|
|
|
.unwrap_or(IpAddr::V4(Ipv4Addr::UNSPECIFIED));
|
|
|
|
|
|
|
|
let hooks = config.hooks.iter().filter(|(name, hook)| {
|
|
|
|
if let Some(ip) = &hook.ip_filter {
|
2021-06-30 23:52:19 +02:00
|
|
|
accept_ip(name, client_ip, ip)
|
2021-05-29 00:50:48 +02:00
|
|
|
} else {
|
2021-06-02 03:35:43 +02:00
|
|
|
info!(
|
|
|
|
"Allow hook `{}` from {}, no IP filter was configured",
|
|
|
|
&name, &client_ip
|
|
|
|
);
|
|
|
|
true
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
for (hook_name, hook) in hooks {
|
|
|
|
let signature = request
|
|
|
|
.headers()
|
|
|
|
.get_one(&hook.signature)
|
|
|
|
.ok_or(WebhookeyError::InvalidSignature)?;
|
|
|
|
|
|
|
|
let secrets = hook
|
|
|
|
.secrets
|
|
|
|
.iter()
|
2021-06-30 23:52:19 +02:00
|
|
|
.map(|secret| validate_request(secret, signature, &buffer));
|
2021-06-02 03:35:43 +02:00
|
|
|
|
|
|
|
for secret in secrets {
|
|
|
|
match secret {
|
|
|
|
Ok(()) => {
|
|
|
|
trace!("Valid signature found for hook `{}`", hook_name);
|
|
|
|
|
|
|
|
valid = true;
|
|
|
|
|
|
|
|
let mut data: serde_json::Value =
|
|
|
|
serde_json::from_slice(&buffer).map_err(WebhookeyError::Serde)?;
|
|
|
|
|
|
|
|
match hook.filter.evaluate(request, &data) {
|
2021-06-30 23:52:19 +02:00
|
|
|
Ok(true) => match hook.get_command(hook_name, request, &mut data) {
|
2021-06-02 03:35:43 +02:00
|
|
|
Ok(command) => {
|
|
|
|
info!("Filter for `{}` matched", &hook_name);
|
|
|
|
result.insert(hook_name.to_string(), command);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
Err(e) => error!("{}", e),
|
|
|
|
},
|
|
|
|
Ok(false) => info!("Filter for `{}` did not match", &hook_name),
|
|
|
|
Err(error) => {
|
|
|
|
error!("Could not match filter for `{}`: {}", &hook_name, error)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Err(e) => trace!("Hook `{}` could not validate request: {}", &hook_name, e),
|
|
|
|
}
|
2021-04-02 00:25:39 +02:00
|
|
|
}
|
|
|
|
}
|
2021-06-02 03:35:43 +02:00
|
|
|
|
|
|
|
if !valid {
|
|
|
|
return Err(WebhookeyError::Unauthorized(*client_ip));
|
2021-04-02 00:25:39 +02:00
|
|
|
}
|
2021-06-02 03:35:43 +02:00
|
|
|
|
|
|
|
Ok(Hooks { inner: result })
|
2021-04-02 00:25:39 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-06-02 03:35:43 +02:00
|
|
|
impl FromDataSimple for Hooks {
|
|
|
|
type Error = WebhookeyError;
|
|
|
|
|
|
|
|
fn from_data(request: &Request, data: Data) -> data::Outcome<Self, Self::Error> {
|
2021-06-30 23:52:19 +02:00
|
|
|
match Hooks::get_commands(request, data) {
|
2021-06-02 03:35:43 +02:00
|
|
|
Ok(hooks) => {
|
|
|
|
if hooks.inner.is_empty() {
|
|
|
|
let client_ip = &request
|
|
|
|
.client_ip()
|
|
|
|
.unwrap_or(IpAddr::V4(Ipv4Addr::UNSPECIFIED));
|
|
|
|
|
|
|
|
warn!("Unmatched hook from {}", &client_ip);
|
|
|
|
return Failure((Status::NotFound, WebhookeyError::UnmatchedHook(*client_ip)));
|
|
|
|
}
|
|
|
|
|
|
|
|
Success(hooks)
|
|
|
|
}
|
|
|
|
Err(WebhookeyError::Unauthorized(e)) => {
|
|
|
|
error!("{}", WebhookeyError::Unauthorized(e));
|
|
|
|
Failure((Status::Unauthorized, WebhookeyError::Unauthorized(e)))
|
|
|
|
}
|
|
|
|
Err(e) => {
|
|
|
|
error!("{}", e);
|
|
|
|
Failure((Status::BadRequest, e))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn accept_ip(hook_name: &str, client_ip: &IpAddr, ip: &IpFilter) -> bool {
|
|
|
|
if ip.validate(client_ip) {
|
|
|
|
info!("Allow hook `{}` from {}", &hook_name, &client_ip);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
warn!("Deny hook `{}` from {}", &hook_name, &client_ip);
|
|
|
|
false
|
|
|
|
}
|
|
|
|
|
2021-03-29 02:19:30 +02:00
|
|
|
fn validate_request(secret: &str, signature: &str, data: &[u8]) -> Result<()> {
|
2021-06-30 23:52:19 +02:00
|
|
|
let mut mac = Hmac::<Sha256>::new_varkey(secret.as_bytes())
|
2021-03-29 02:19:30 +02:00
|
|
|
.map_err(|e| anyhow!("Could not create hasher with secret: {}", e))?;
|
2021-06-30 23:52:19 +02:00
|
|
|
mac.update(data);
|
2021-03-29 02:19:30 +02:00
|
|
|
let raw_signature = hex::decode(signature.as_bytes())?;
|
|
|
|
mac.verify(&raw_signature).map_err(|e| anyhow!("{}", e))
|
|
|
|
}
|
|
|
|
|
2021-04-16 09:58:15 +02:00
|
|
|
fn get_parameter(input: &str) -> Result<Vec<&str>> {
|
|
|
|
let parse: IResult<&str, Vec<&str>> = many0(alt((
|
|
|
|
delimited(tag("{{"), take_until("}}"), tag("}}")),
|
|
|
|
take_until("{{"),
|
2021-06-30 23:52:19 +02:00
|
|
|
)))(input);
|
2021-04-16 09:58:15 +02:00
|
|
|
|
|
|
|
let (_last, result) = parse
|
|
|
|
.finish()
|
|
|
|
.map_err(|e| anyhow!("Could not get parameters from command: {}", e))?;
|
|
|
|
|
|
|
|
Ok(result)
|
|
|
|
}
|
|
|
|
|
2021-05-29 00:50:48 +02:00
|
|
|
fn get_header_field<'a>(headers: &'a HeaderMap, param: &[&str]) -> Result<&'a str> {
|
|
|
|
headers
|
|
|
|
.get_one(
|
|
|
|
param
|
|
|
|
.get(1)
|
|
|
|
.ok_or_else(|| anyhow!("Missing parameter for `header` expression"))?,
|
|
|
|
)
|
|
|
|
.ok_or_else(|| anyhow!("Could not extract event parameter from header"))
|
|
|
|
}
|
|
|
|
|
|
|
|
fn get_value_from_pointer<'a>(data: &'a serde_json::Value, pointer: &'a str) -> Result<&'a str> {
|
|
|
|
let value = data
|
|
|
|
.pointer(pointer)
|
|
|
|
.ok_or_else(|| anyhow!("Could not get field from pointer {}", pointer))?;
|
|
|
|
|
|
|
|
value
|
|
|
|
.as_str()
|
|
|
|
.ok_or_else(|| anyhow!("Could not convert value `{}` to string", value))
|
|
|
|
}
|
|
|
|
|
|
|
|
fn replace_parameters(
|
|
|
|
input: &str,
|
|
|
|
headers: &HeaderMap,
|
|
|
|
data: &serde_json::Value,
|
|
|
|
) -> Result<String> {
|
2021-03-21 15:51:58 +01:00
|
|
|
let parse: IResult<&str, Vec<&str>> = many0(alt((
|
|
|
|
map_res(
|
|
|
|
delimited(tag("{{"), take_until("}}"), tag("}}")),
|
2021-03-30 01:16:15 +02:00
|
|
|
|param: &str| {
|
|
|
|
let expr = param.trim().split(' ').collect::<Vec<&str>>();
|
|
|
|
|
|
|
|
match expr.get(0) {
|
2021-05-29 00:50:48 +02:00
|
|
|
Some(&"header") => get_header_field(headers, &expr),
|
2021-06-30 23:52:19 +02:00
|
|
|
Some(pointer) => get_value_from_pointer(data, pointer),
|
2021-03-30 01:16:15 +02:00
|
|
|
None => bail!("Missing expression in `{}`", input),
|
|
|
|
}
|
2021-03-21 15:51:58 +01:00
|
|
|
},
|
|
|
|
),
|
|
|
|
take_until("{{"),
|
|
|
|
)))(input);
|
|
|
|
|
|
|
|
let (last, mut result) = parse
|
|
|
|
.finish()
|
|
|
|
.map_err(|e| anyhow!("Could not parse command: {}", e))?;
|
|
|
|
result.push(last);
|
|
|
|
|
|
|
|
Ok(result.join(""))
|
|
|
|
}
|
|
|
|
|
2021-05-31 02:16:22 +02:00
|
|
|
fn get_string(value: &serde_json::Value) -> Result<String, WebhookeyError> {
|
2021-04-16 09:58:15 +02:00
|
|
|
match &value {
|
2021-04-22 11:52:02 +02:00
|
|
|
serde_json::Value::Bool(bool) => Ok(bool.to_string()),
|
2021-04-16 09:58:15 +02:00
|
|
|
serde_json::Value::Number(number) => Ok(number.to_string()),
|
|
|
|
serde_json::Value::String(string) => Ok(string.as_str().to_string()),
|
2021-05-29 00:50:48 +02:00
|
|
|
x => {
|
|
|
|
error!("Could not get string from: {:?}", x);
|
|
|
|
unimplemented!()
|
|
|
|
}
|
2021-04-13 16:55:13 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-28 03:50:52 +02:00
|
|
|
#[post("/", format = "json", data = "<hooks>")]
|
|
|
|
fn receive_hook<'a>(address: SocketAddr, hooks: Hooks) -> Result<Response<'a>> {
|
2021-03-20 00:12:01 +01:00
|
|
|
info!("Post request received from: {}", address);
|
2021-03-03 16:14:54 +01:00
|
|
|
|
2021-05-31 02:16:22 +02:00
|
|
|
hooks.inner.iter().for_each(|(name, command)| {
|
2021-05-29 00:50:48 +02:00
|
|
|
info!("Execute `{}` from hook `{}`", &command, &name);
|
2021-03-29 04:21:31 +02:00
|
|
|
|
2021-06-30 23:52:19 +02:00
|
|
|
match run_script::run(command, &vec![], &ScriptOptions::new()) {
|
2021-04-16 17:42:40 +02:00
|
|
|
Ok((status, stdout, stderr)) => {
|
2021-05-29 00:50:48 +02:00
|
|
|
info!("Command `{}` exited with return code: {}", &command, status);
|
|
|
|
trace!("Output of command `{}` on stdout: {:?}", &command, &stdout);
|
|
|
|
debug!("Output of command `{}` on stderr: {:?}", &command, &stderr);
|
2021-03-29 04:21:31 +02:00
|
|
|
}
|
|
|
|
Err(e) => {
|
2021-05-29 00:50:48 +02:00
|
|
|
error!("Execution of `{}` failed: {}", &command, e);
|
2021-03-19 10:16:46 +01:00
|
|
|
}
|
|
|
|
}
|
2021-05-31 02:16:22 +02:00
|
|
|
});
|
2021-03-03 15:24:46 +01:00
|
|
|
|
2021-03-28 03:50:52 +02:00
|
|
|
Ok(Response::new())
|
2021-02-02 11:05:50 +01:00
|
|
|
}
|
|
|
|
|
2021-03-03 15:24:46 +01:00
|
|
|
fn get_config() -> Result<File> {
|
2021-05-31 02:16:22 +02:00
|
|
|
// Look for config in CWD..
|
|
|
|
if let Ok(config) = File::open("config.yml") {
|
|
|
|
info!("Loading configuration from `./config.yml`");
|
2021-03-03 16:14:54 +01:00
|
|
|
|
2021-03-03 15:24:46 +01:00
|
|
|
return Ok(config);
|
|
|
|
}
|
|
|
|
|
2021-05-29 00:50:48 +02:00
|
|
|
// ..look for user path config..
|
2021-03-03 15:24:46 +01:00
|
|
|
if let Some(mut path) = dirs::config_dir() {
|
|
|
|
path.push("webhookey/config.yml");
|
|
|
|
|
2021-03-03 16:14:54 +01:00
|
|
|
if let Ok(config) = File::open(&path) {
|
|
|
|
info!(
|
|
|
|
"Loading configuration from `{}`",
|
2021-03-21 15:51:58 +01:00
|
|
|
path.to_str().unwrap_or("<path unprintable>"),
|
2021-03-03 16:14:54 +01:00
|
|
|
);
|
|
|
|
|
2021-03-03 15:24:46 +01:00
|
|
|
return Ok(config);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-05-31 02:16:22 +02:00
|
|
|
// ..look for systemwide config..
|
|
|
|
if let Ok(config) = File::open("/etc/webhookey/config.yml") {
|
|
|
|
info!("Loading configuration from `/etc/webhookey/config.yml`");
|
2021-03-03 16:14:54 +01:00
|
|
|
|
2021-03-03 15:24:46 +01:00
|
|
|
return Ok(config);
|
|
|
|
}
|
|
|
|
|
2021-05-29 00:50:48 +02:00
|
|
|
// ..you had your chance.
|
2021-03-30 01:16:15 +02:00
|
|
|
bail!("No configuration file found.");
|
2021-03-03 15:24:46 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
fn main() -> Result<()> {
|
2021-03-03 16:14:54 +01:00
|
|
|
env_logger::init();
|
|
|
|
|
2021-06-30 23:55:21 +02:00
|
|
|
let cli: Opts = Opts::parse();
|
2021-05-31 16:14:19 +02:00
|
|
|
|
2021-06-30 23:55:21 +02:00
|
|
|
let config: Config = match cli.config {
|
2021-05-31 16:14:19 +02:00
|
|
|
Some(config) => serde_yaml::from_reader(BufReader::new(File::open(config)?))?,
|
|
|
|
_ => serde_yaml::from_reader(BufReader::new(get_config()?))?,
|
|
|
|
};
|
2021-03-03 15:24:46 +01:00
|
|
|
|
2021-03-03 16:14:54 +01:00
|
|
|
trace!("Parsed configuration:\n{}", serde_yaml::to_string(&config)?);
|
2021-03-03 15:24:46 +01:00
|
|
|
|
2021-06-30 23:55:21 +02:00
|
|
|
if cli.command.is_some() {
|
2021-05-31 16:14:19 +02:00
|
|
|
debug!("Configtest succeded.");
|
|
|
|
println!("Config is OK");
|
2021-06-02 03:35:43 +02:00
|
|
|
return Ok(());
|
2021-05-31 16:14:19 +02:00
|
|
|
}
|
|
|
|
|
2021-02-02 11:05:50 +01:00
|
|
|
rocket::ignite()
|
2021-05-29 00:50:48 +02:00
|
|
|
.mount("/", routes![receive_hook])
|
2021-03-03 15:24:46 +01:00
|
|
|
.attach(AdHoc::on_attach("webhookey config", move |rocket| {
|
|
|
|
Ok(rocket.manage(config))
|
|
|
|
}))
|
2021-02-02 11:05:50 +01:00
|
|
|
.launch();
|
2021-03-03 15:24:46 +01:00
|
|
|
|
|
|
|
Ok(())
|
2021-02-02 11:05:50 +01:00
|
|
|
}
|
2021-03-20 00:12:01 +01:00
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use super::*;
|
2021-03-28 03:50:52 +02:00
|
|
|
use rocket::{
|
|
|
|
http::{ContentType, Header},
|
|
|
|
local::Client,
|
|
|
|
};
|
2021-03-21 15:51:58 +01:00
|
|
|
use serde_json::json;
|
2021-03-20 00:12:01 +01:00
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn secret() {
|
|
|
|
let mut hooks = HashMap::new();
|
2021-05-31 02:16:22 +02:00
|
|
|
|
2021-03-20 00:12:01 +01:00
|
|
|
hooks.insert(
|
|
|
|
"test_hook".to_string(),
|
|
|
|
Hook {
|
2021-03-28 03:50:52 +02:00
|
|
|
command: "".to_string(),
|
2021-03-29 04:21:31 +02:00
|
|
|
signature: "X-Gitea-Signature".to_string(),
|
2021-04-02 00:25:39 +02:00
|
|
|
ip_filter: None,
|
2021-03-20 00:12:01 +01:00
|
|
|
secrets: vec!["valid".to_string()],
|
2021-05-31 02:16:22 +02:00
|
|
|
filter: FilterType::JsonFilter(JsonFilter {
|
|
|
|
pointer: "*".to_string(),
|
|
|
|
regex: "*".to_string(),
|
|
|
|
}),
|
2021-03-20 00:12:01 +01:00
|
|
|
},
|
|
|
|
);
|
2021-05-31 02:16:22 +02:00
|
|
|
|
2021-03-20 00:12:01 +01:00
|
|
|
let config = Config { hooks: hooks };
|
|
|
|
|
|
|
|
let rocket = rocket::ignite()
|
|
|
|
.mount("/", routes![receive_hook])
|
|
|
|
.attach(AdHoc::on_attach("webhookey config", move |rocket| {
|
|
|
|
Ok(rocket.manage(config))
|
|
|
|
}));
|
|
|
|
|
|
|
|
let client = Client::new(rocket).unwrap();
|
|
|
|
let response = client
|
|
|
|
.post("/")
|
2021-03-28 03:50:52 +02:00
|
|
|
.header(Header::new(
|
|
|
|
"X-Gitea-Signature",
|
|
|
|
"28175a0035f637f3cbb85afee9f9d319631580e7621cf790cd16ca063a2f820e",
|
|
|
|
))
|
2021-03-20 00:12:01 +01:00
|
|
|
.header(ContentType::JSON)
|
|
|
|
.remote("127.0.0.1:8000".parse().unwrap())
|
2021-03-28 03:50:52 +02:00
|
|
|
.body(&serde_json::to_string(&json!({ "foo": "bar" })).unwrap())
|
2021-03-20 00:12:01 +01:00
|
|
|
.dispatch();
|
|
|
|
|
2021-03-28 03:50:52 +02:00
|
|
|
assert_eq!(response.status(), Status::NotFound);
|
2021-03-20 00:12:01 +01:00
|
|
|
|
|
|
|
let response = client
|
|
|
|
.post("/")
|
2021-03-28 03:50:52 +02:00
|
|
|
.header(Header::new("X-Gitea-Signature", "beef"))
|
2021-03-20 00:12:01 +01:00
|
|
|
.header(ContentType::JSON)
|
|
|
|
.remote("127.0.0.1:8000".parse().unwrap())
|
2021-03-28 03:50:52 +02:00
|
|
|
.body(&serde_json::to_string(&json!({ "foo": "bar" })).unwrap())
|
2021-03-20 00:12:01 +01:00
|
|
|
.dispatch();
|
|
|
|
|
|
|
|
assert_eq!(response.status(), Status::Unauthorized);
|
|
|
|
|
|
|
|
let response = client
|
|
|
|
.post("/")
|
2021-03-28 03:50:52 +02:00
|
|
|
.header(Header::new(
|
|
|
|
"X-Gitea-Signature",
|
|
|
|
"c5c315d76318362ec129ca629b50b626bba09ad3d7ba4cc0f4c0afe4a90537a0",
|
|
|
|
))
|
2021-03-20 00:12:01 +01:00
|
|
|
.header(ContentType::JSON)
|
|
|
|
.remote("127.0.0.1:8000".parse().unwrap())
|
|
|
|
.body(r#"{ "not_secret": "invalid" "#)
|
|
|
|
.dispatch();
|
|
|
|
|
|
|
|
assert_eq!(response.status(), Status::BadRequest);
|
2021-03-29 02:19:30 +02:00
|
|
|
|
|
|
|
let response = client
|
|
|
|
.post("/")
|
|
|
|
.header(Header::new("X-Gitea-Signature", "foobar"))
|
|
|
|
.header(ContentType::JSON)
|
|
|
|
.remote("127.0.0.1:8000".parse().unwrap())
|
|
|
|
.dispatch();
|
|
|
|
|
|
|
|
assert_eq!(response.status(), Status::Unauthorized);
|
2021-03-20 00:12:01 +01:00
|
|
|
}
|
2021-03-21 15:51:58 +01:00
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn parse_command() {
|
2021-03-28 03:50:52 +02:00
|
|
|
let mut map = HeaderMap::new();
|
|
|
|
map.add_raw("X-Gitea-Event", "something");
|
|
|
|
|
2021-03-21 15:51:58 +01:00
|
|
|
assert_eq!(
|
2021-05-29 00:50:48 +02:00
|
|
|
replace_parameters("command", &map, &serde_json::Value::Null).unwrap(),
|
2021-03-21 15:51:58 +01:00
|
|
|
"command"
|
|
|
|
);
|
|
|
|
|
|
|
|
assert_eq!(
|
2021-05-29 00:50:48 +02:00
|
|
|
replace_parameters(" command", &map, &serde_json::Value::Null).unwrap(),
|
2021-03-21 15:51:58 +01:00
|
|
|
" command"
|
|
|
|
);
|
|
|
|
|
|
|
|
assert_eq!(
|
2021-05-29 00:50:48 +02:00
|
|
|
replace_parameters("command ", &map, &serde_json::Value::Null).unwrap(),
|
2021-03-21 15:51:58 +01:00
|
|
|
"command "
|
|
|
|
);
|
|
|
|
|
|
|
|
assert_eq!(
|
2021-05-29 00:50:48 +02:00
|
|
|
replace_parameters(" command ", &map, &serde_json::Value::Null).unwrap(),
|
2021-03-21 15:51:58 +01:00
|
|
|
" command "
|
|
|
|
);
|
|
|
|
|
|
|
|
assert_eq!(
|
2021-05-29 00:50:48 +02:00
|
|
|
replace_parameters("command command ", &map, &serde_json::Value::Null).unwrap(),
|
2021-03-21 15:51:58 +01:00
|
|
|
"command command "
|
|
|
|
);
|
|
|
|
|
|
|
|
assert_eq!(
|
2021-05-29 00:50:48 +02:00
|
|
|
replace_parameters("{{ /foo }} command", &map, &json!({ "foo": "bar" })).unwrap(),
|
2021-03-21 15:51:58 +01:00
|
|
|
"bar command"
|
|
|
|
);
|
|
|
|
|
|
|
|
assert_eq!(
|
2021-05-29 00:50:48 +02:00
|
|
|
replace_parameters(" command {{ /foo }} ", &map, &json!({ "foo": "bar" })).unwrap(),
|
2021-03-21 15:51:58 +01:00
|
|
|
" command bar "
|
|
|
|
);
|
|
|
|
|
|
|
|
assert_eq!(
|
2021-05-29 00:50:48 +02:00
|
|
|
replace_parameters(
|
2021-03-21 15:51:58 +01:00
|
|
|
"{{ /foo }} command{{/field1/foo}}",
|
2021-03-28 03:50:52 +02:00
|
|
|
&map,
|
2021-03-21 15:51:58 +01:00
|
|
|
&json!({ "foo": "bar", "field1": { "foo": "baz" } })
|
|
|
|
)
|
|
|
|
.unwrap(),
|
|
|
|
"bar commandbaz"
|
|
|
|
);
|
|
|
|
|
|
|
|
assert_eq!(
|
2021-05-29 00:50:48 +02:00
|
|
|
replace_parameters(" command {{ /foo }} ", &map, &json!({ "foo": "bar" })).unwrap(),
|
2021-03-21 15:51:58 +01:00
|
|
|
" command bar "
|
|
|
|
);
|
|
|
|
|
|
|
|
assert_eq!(
|
2021-05-29 00:50:48 +02:00
|
|
|
replace_parameters(
|
2021-03-21 15:51:58 +01:00
|
|
|
" {{ /field1/foo }} command",
|
2021-03-28 03:50:52 +02:00
|
|
|
&map,
|
2021-03-21 15:51:58 +01:00
|
|
|
&json!({ "field1": { "foo": "bar" } })
|
|
|
|
)
|
|
|
|
.unwrap(),
|
|
|
|
" bar command"
|
|
|
|
);
|
2021-03-28 03:50:52 +02:00
|
|
|
|
2021-03-29 04:21:31 +02:00
|
|
|
assert_eq!(
|
2021-05-29 00:50:48 +02:00
|
|
|
replace_parameters(
|
2021-03-30 01:16:15 +02:00
|
|
|
" {{ header X-Gitea-Event }} command",
|
2021-03-29 04:21:31 +02:00
|
|
|
&map,
|
|
|
|
&json!({ "field1": { "foo": "bar" } })
|
|
|
|
)
|
|
|
|
.unwrap(),
|
|
|
|
" something command"
|
|
|
|
);
|
2021-03-21 15:51:58 +01:00
|
|
|
}
|
2021-03-20 00:12:01 +01:00
|
|
|
}
|