Use signature field for verification

Instead of looking for a "secret" field hmac is used. Therefore the
raw payload is hashed with all secrets consecutively in order to
validate its content. If the content is certified the established
behaviour is pursued..
This commit is contained in:
finga 2021-03-28 03:50:52 +02:00
parent a130bdc125
commit ee32424f8c
4 changed files with 297 additions and 372 deletions

272
Cargo.lock generated
View file

@ -91,7 +91,7 @@ checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
dependencies = [
"hermit-abi",
"libc",
"winapi 0.3.9",
"winapi",
]
[[package]]
@ -172,12 +172,6 @@ version = "1.0.67"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd"
[[package]]
name = "cfg-if"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
[[package]]
name = "cfg-if"
version = "1.0.0"
@ -234,7 +228,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7e9d99fa91428effe99c5c6d4634cdeba32b8cf784fc428a2a687f61a952c49"
dependencies = [
"autocfg",
"cfg-if 1.0.0",
"cfg-if",
"lazy_static",
]
@ -315,7 +309,7 @@ checksum = "8e93d7f5705de3e49895a2b5e0b8855a1c27f080192ae9c32a6432d50741a57a"
dependencies = [
"libc",
"redox_users",
"winapi 0.3.9",
"winapi",
]
[[package]]
@ -337,53 +331,6 @@ dependencies = [
"termcolor",
]
[[package]]
name = "filetime"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d34cfa13a63ae058bfa601fe9e313bbdb3746427c1459185464ce0fcf62e1e8"
dependencies = [
"cfg-if 1.0.0",
"libc",
"redox_syscall 0.2.5",
"winapi 0.3.9",
]
[[package]]
name = "fsevent"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ab7d1bd1bd33cc98b0889831b72da23c0aa4df9cec7e0702f46ecea04b35db6"
dependencies = [
"bitflags",
"fsevent-sys",
]
[[package]]
name = "fsevent-sys"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f41b048a94555da0f42f1d632e2e19510084fb8e303b0daa2816e733fb3644a0"
dependencies = [
"libc",
]
[[package]]
name = "fuchsia-zircon"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82"
dependencies = [
"bitflags",
"fuchsia-zircon-sys",
]
[[package]]
name = "fuchsia-zircon-sys"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7"
[[package]]
name = "funty"
version = "1.1.0"
@ -406,7 +353,7 @@ version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
dependencies = [
"cfg-if 1.0.0",
"cfg-if",
"libc",
"wasi 0.9.0+wasi-snapshot-preview1",
]
@ -417,7 +364,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9495705279e7140bf035dde1f6e750c162df8b625267cd52cc44e0b156732c8"
dependencies = [
"cfg-if 1.0.0",
"cfg-if",
"libc",
"wasi 0.10.2+wasi-snapshot-preview1",
]
@ -453,6 +400,12 @@ dependencies = [
"libc",
]
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hkdf"
version = "0.10.0"
@ -537,51 +490,12 @@ dependencies = [
"hashbrown",
]
[[package]]
name = "inotify"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4816c66d2c8ae673df83366c18341538f234a26d65a9ecea5c348b453ac1d02f"
dependencies = [
"bitflags",
"inotify-sys",
"libc",
]
[[package]]
name = "inotify-sys"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
dependencies = [
"libc",
]
[[package]]
name = "iovec"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e"
dependencies = [
"libc",
]
[[package]]
name = "itoa"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736"
[[package]]
name = "kernel32-sys"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d"
dependencies = [
"winapi 0.2.8",
"winapi-build",
]
[[package]]
name = "language-tags"
version = "0.2.2"
@ -594,12 +508,6 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "lazycell"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
[[package]]
name = "lexical-core"
version = "0.7.5"
@ -608,7 +516,7 @@ checksum = "21f866863575d0e1d654fbeeabdc927292fdf862873dc3c96c6f753357e13374"
dependencies = [
"arrayvec",
"bitflags",
"cfg-if 1.0.0",
"cfg-if",
"ryu",
"static_assertions",
]
@ -640,7 +548,7 @@ version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
dependencies = [
"cfg-if 1.0.0",
"cfg-if",
]
[[package]]
@ -664,60 +572,6 @@ dependencies = [
"log 0.3.9",
]
[[package]]
name = "mio"
version = "0.6.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4afd66f5b91bf2a3bc13fad0e21caedac168ca4c707504e75585648ae80e4cc4"
dependencies = [
"cfg-if 0.1.10",
"fuchsia-zircon",
"fuchsia-zircon-sys",
"iovec",
"kernel32-sys",
"libc",
"log 0.4.14",
"miow",
"net2",
"slab",
"winapi 0.2.8",
]
[[package]]
name = "mio-extras"
version = "2.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52403fe290012ce777c4626790c8951324a2b9e3316b3143779c72b029742f19"
dependencies = [
"lazycell",
"log 0.4.14",
"mio",
"slab",
]
[[package]]
name = "miow"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebd808424166322d4a38da87083bfddd3ac4c131334ed55856112eb06d46944d"
dependencies = [
"kernel32-sys",
"net2",
"winapi 0.2.8",
"ws2_32-sys",
]
[[package]]
name = "net2"
version = "0.2.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "391630d12b68002ae1e25e8f974306474966550ad82dac6886fb8910c19568ae"
dependencies = [
"cfg-if 0.1.10",
"libc",
"winapi 0.3.9",
]
[[package]]
name = "nom"
version = "6.1.2"
@ -731,24 +585,6 @@ dependencies = [
"version_check 0.9.3",
]
[[package]]
name = "notify"
version = "4.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80ae4a7688d1fab81c5bf19c64fc8db920be8d519ce6336ed4e7efe024724dbd"
dependencies = [
"bitflags",
"filetime",
"fsevent",
"fsevent-sys",
"inotify",
"libc",
"mio",
"mio-extras",
"walkdir",
"winapi 0.3.9",
]
[[package]]
name = "num_cpus"
version = "1.13.0"
@ -904,15 +740,6 @@ version = "0.1.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce"
[[package]]
name = "redox_syscall"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94341e4e44e24f6b591b59e47a8a027df12e008d73fd5672dbea9cc22f4507d9"
dependencies = [
"bitflags",
]
[[package]]
name = "redox_users"
version = "0.3.5"
@ -920,7 +747,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d"
dependencies = [
"getrandom 0.1.16",
"redox_syscall 0.1.57",
"redox_syscall",
"rust-argon2",
]
@ -989,19 +816,6 @@ dependencies = [
"yansi",
]
[[package]]
name = "rocket_contrib"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7954a707f9ca18aa74ca8c1f5d1f900f52a4dceb68e96e3112143f759cfd20e"
dependencies = [
"log 0.4.14",
"notify",
"rocket",
"serde",
"serde_json",
]
[[package]]
name = "rocket_http"
version = "0.4.7"
@ -1059,15 +873,6 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "sct"
version = "0.4.0"
@ -1128,18 +933,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa827a14b29ab7f44778d14a88d3cb76e949c45083f7dbfa507d0cb699dc12de"
dependencies = [
"block-buffer",
"cfg-if 1.0.0",
"cfg-if",
"cpuid-bool 0.1.2",
"digest",
"opaque-debug",
]
[[package]]
name = "slab"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8"
[[package]]
name = "smallvec"
version = "1.6.1"
@ -1208,7 +1007,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438"
dependencies = [
"libc",
"winapi 0.3.9",
"winapi",
]
[[package]]
@ -1331,17 +1130,6 @@ version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
[[package]]
name = "walkdir"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "777182bc735b6424e1a57516d35ed72cb8019d85c8c9bf536dccb3445c1a2f7d"
dependencies = [
"same-file",
"winapi 0.3.9",
"winapi-util",
]
[[package]]
name = "wasi"
version = "0.9.0+wasi-snapshot-preview1"
@ -1361,14 +1149,16 @@ dependencies = [
"anyhow",
"dirs",
"env_logger",
"hex",
"hmac",
"log 0.4.14",
"nom",
"regex",
"rocket",
"rocket_contrib",
"serde",
"serde_json",
"serde_yaml",
"sha2",
]
[[package]]
@ -1391,12 +1181,6 @@ dependencies = [
"webpki",
]
[[package]]
name = "winapi"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a"
[[package]]
name = "winapi"
version = "0.3.9"
@ -1407,12 +1191,6 @@ dependencies = [
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-build"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc"
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
@ -1425,7 +1203,7 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
dependencies = [
"winapi 0.3.9",
"winapi",
]
[[package]]
@ -1434,16 +1212,6 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "ws2_32-sys"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e"
dependencies = [
"winapi 0.2.8",
"winapi-build",
]
[[package]]
name = "wyz"
version = "0.2.0"

View file

@ -12,7 +12,6 @@ tls = ["rocket/tls"]
[dependencies]
rocket = "0.4"
rocket_contrib = { version = "0.4", default-features = false, features = ["json"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_yaml = "0.8"
@ -22,3 +21,6 @@ anyhow = "1.0"
log = "0.4"
env_logger = "0.8"
nom = "6"
hmac = "0.10"
sha2 = "0.9"
hex = "0.4"

View file

@ -55,11 +55,12 @@ Configuration syntax is YAML and has to be done in following order:
Right now there is only the configuration parameter for hooks, here
each hook has to be configured, It contains following fields:
- command: Optional string for a command to be executed when all
filters match. Pointers ([RFC
6901](https://tools.ietf.org/html/rfc6901)) to JSON fields may be
used to be replaced with data from the JSON data with `{{
/field/pointed/to }}`
- command: String for a command to be executed when all filters
match. Pointers ([RFC 6901](https://tools.ietf.org/html/rfc6901)) to
JSON fields may be used to be replaced with data from the JSON data
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.
- secrets: List of secrets.
- filters: List of filters.
@ -81,7 +82,6 @@ Whereas `<config_dir>` depends on the platform:
- Windows: `{FOLDERID_RoamingAppData}`
# TODOs
## Use `lazy_static` or `once_cell` for compiled regexes
## Use `clap` to parse command line arguments
## Implement the functionality to reply to certain webhooks
## Configure rocket via config.yml

View file

@ -1,7 +1,8 @@
#![feature(proc_macro_hygiene, decl_macro)]
use anyhow::{anyhow, bail, Result};
use log::{debug, info, trace, warn};
use hmac::{Hmac, Mac, NewMac};
use log::{debug, error, info, trace, warn};
use nom::{
branch::alt,
bytes::complete::{tag, take_until},
@ -11,12 +12,24 @@ use nom::{
Finish, IResult,
};
use regex::Regex;
use rocket::{fairing::AdHoc, get, http::Status, post, routes, Response, State};
use rocket_contrib::json::Json;
use rocket::{
data::{self, FromDataSimple},
fairing::AdHoc,
get,
http::{HeaderMap, Status},
post, routes, Data,
Outcome::{Failure, Success},
Request, Response, State,
};
use serde::{Deserialize, Serialize};
use sha2::Sha256;
use std::{
collections::HashMap, fs::File, io::BufReader, net::SocketAddr, process::Command,
collections::HashMap,
fs::File,
io::{BufReader, Read},
net::SocketAddr,
process::Command,
str::from_utf8,
};
@ -27,7 +40,7 @@ struct Config {
#[derive(Debug, Deserialize, Serialize)]
struct Hook {
command: Option<String>,
command: String,
secrets: Vec<String>,
filters: HashMap<String, Filter>,
}
@ -38,28 +51,28 @@ struct Filter {
regex: String,
}
#[derive(Debug, Deserialize, Serialize)]
struct Data(serde_json::Value);
#[derive(Debug)]
struct Hooks(HashMap<String, Vec<String>>);
#[get("/")]
fn index() -> &'static str {
"Hello, webhookey!"
}
fn replace_parameter(input: &str, data: &serde_json::Value) -> Result<String> {
fn replace_parameter(input: &str, headers: &HeaderMap, data: &serde_json::Value) -> Result<String> {
let parse: IResult<&str, Vec<&str>> = many0(alt((
map_res(
delimited(tag("{{"), take_until("}}"), tag("}}")),
|param: &str| {
if let Some(value) = data.pointer(param.trim()) {
if let Some(value) = value.as_str() {
Ok(value)
|param: &str| match param.trim() {
"event" => {
if let Some(event) = headers.get_one("X-Gitea-Event") {
Ok(event)
} else {
bail!("Could not convert field `{}` to string", param.trim());
bail!("Could not extract event parameter from header");
}
} else {
bail!("Could not find `{}` in received data", param.trim());
}
pointer => match data.pointer(pointer) {
Some(value) => match value.as_str() {
Some(value) => Ok(value),
_ => bail!("Could not convert value `{}` to string", value),
},
_ => bail!("Could not convert field `{}` to string", param.trim()),
},
},
),
take_until("{{"),
@ -73,92 +86,224 @@ fn replace_parameter(input: &str, data: &serde_json::Value) -> Result<String> {
Ok(result.join(""))
}
fn execute_hook(name: &str, hook: &Hook, data: &serde_json::Value) -> Result<()> {
debug!("Running hook `{}`", name);
impl FromDataSimple for Hooks {
type Error = anyhow::Error;
for (filter_name, filter) in hook.filters.iter() {
debug!("Matching filter `{}`", filter_name);
fn from_data(request: &Request, data: Data) -> data::Outcome<Self, Self::Error> {
let config = request.guard::<State<Config>>().unwrap(); // should never fail
if let Some(value) = data.pointer(&filter.pointer) {
let regex = Regex::new(&filter.regex)?;
let mut hooks = HashMap::new();
if let Some(value) = value.as_str() {
if !regex.is_match(value) {
info!("Filter `{}` in hook `{}` did not match", filter_name, name);
return Ok(());
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),
));
}
} else {
anyhow!(
"Could not parse pointer in hook `{}` from filter `{}`",
name,
filter_name
);
}
}
}
if let Some(command) = &hook.command {
let command = replace_parameter(&command, data)?;
trace!("Data received: {:?}", from_utf8(&buffer));
info!("Execute `{}` from hook `{}`", command, name);
let mut valid = false;
let command = command.split(' ').collect::<Vec<&str>>();
let exec_command = Command::new(&command[0]).args(&command[1..]).output()?;
for (hook_name, hook) in &config.hooks {
let mut commands = Vec::new();
info!(
"Command `{}` exited with return code: {}",
&command[0], &exec_command.status
);
trace!(
"Output of command `{}` on stdout: {:?}",
&command[0],
from_utf8(&exec_command.stdout)?
);
debug!(
"Output of command `{}` on stderr: {:?}",
&command[0],
from_utf8(&exec_command.stderr)?
);
}
for secret in &hook.secrets {
let mut mac = match Hmac::<Sha256>::new_varkey(&secret.as_bytes()) {
Ok(mac) => mac,
Err(e) => {
error!("Could not instantiate hasher: {}", e);
return Failure((
Status::InternalServerError,
anyhow!("Could not instantiate hasher: {}", e),
));
}
};
Ok(())
}
mac.update(&buffer);
#[post("/", format = "json", data = "<data>")]
fn receive_hook(address: SocketAddr, config: State<Config>, data: Json<Data>) -> Result<Response> {
info!("Post request received from: {}", address);
match &hex::decode(&signature.as_bytes()) {
Ok(raw_signature) => {
if mac.verify(&raw_signature) == Ok(()) {
trace!(
"Valid signature found for hook `{}`: {}",
hook_name,
signature
);
let mut response = Response::new();
let data = serde_json::to_value(data.0)?;
valid = true;
trace!("Data received from: {}\n{}", address, data);
let data: serde_json::Value = match serde_json::from_slice(&buffer)
{
Ok(data) => data,
Err(e) => {
error!("Could not parse json: {}", e);
return Failure((
Status::BadRequest,
anyhow!("Could not parse json: {}", e),
));
}
};
if let Some(secret) = data.pointer("/secret") {
if let Some(secret) = secret.as_str() {
let hooks: HashMap<&String, &Hook> = config
.hooks
.iter()
.filter(|(_hook_name, hook)| hook.secrets.contains(&secret.to_string()))
.collect();
for (filter_name, filter) in &hook.filters {
trace!(
"Matching filter `{}` of hook `{}`",
filter_name,
hook_name
);
let regex = match Regex::new(&filter.regex) {
Ok(regex) => regex,
Err(e) => {
error!(
"Could not compile regex `{}`: {}",
&filter.regex, e
);
continue;
}
};
if let Some(value) = data.pointer(&filter.pointer) {
if let Some(value) = value.as_str() {
if regex.is_match(value) {
debug!(
"Filter `{}` of hook `{}` matched",
filter_name, hook_name
);
match replace_parameter(
&hook.command.to_string(),
&request.headers(),
&data,
) {
Ok(command) => commands.push(command),
Err(e) => error!(
"Could not replace all parameter in hook `{}`: {}",
hook_name, e
),
}
}
} else {
anyhow!(
"Could not parse pointer in hook `{}` from filter `{}`",
hook_name,
filter_name
);
}
}
trace!(
"Filter `{}` of hook `{}` did not match",
filter_name,
hook_name
);
}
}
}
Err(e) => {
error!("Invalid configuration: {}", e);
return Failure((
Status::InternalServerError,
anyhow!("Invalid configuration: {}", e),
));
}
}
}
if !commands.is_empty() {
hooks.insert(hook_name.to_string(), commands);
}
}
if hooks.is_empty() {
warn!("Secret from {} did not match any hook", address);
response.set_status(Status::Unauthorized);
} else {
for (hook_name, hook) in hooks {
execute_hook(&hook_name, &hook, &data)?;
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))
}
} else {
warn!("Data received from {} contains invalid data", address);
response.set_status(Status::BadRequest);
Failure((
Status::BadRequest,
anyhow!("Could not extract signature from header"),
))
}
}
}
#[get("/")]
fn index() -> &'static str {
"Hello, webhookey!"
}
#[post("/", format = "json", data = "<hooks>")]
fn receive_hook<'a>(address: SocketAddr, hooks: Hooks) -> Result<Response<'a>> {
info!("Post request received from: {}", address);
for hook in hooks.0 {
for command in hook.1 {
info!("Execute `{}` from hook `{}`", &command, &hook.0);
let command = command.split(' ').collect::<Vec<&str>>();
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);
}
}
}
} else {
warn!("Data received from {} did not contain a secret", address);
response.set_status(Status::NotFound);
}
Ok(response)
Ok(Response::new())
}
fn get_config() -> Result<File> {
@ -210,7 +355,10 @@ fn main() -> Result<()> {
#[cfg(test)]
mod tests {
use super::*;
use rocket::{http::ContentType, local::Client};
use rocket::{
http::{ContentType, Header},
local::Client,
};
use serde_json::json;
#[test]
@ -230,7 +378,7 @@ mod tests {
hooks.insert(
"test_hook".to_string(),
Hook {
command: None,
command: "".to_string(),
secrets: vec!["valid".to_string()],
filters: HashMap::new(),
},
@ -246,33 +394,33 @@ mod tests {
let client = Client::new(rocket).unwrap();
let response = client
.post("/")
.header(Header::new(
"X-Gitea-Signature",
"28175a0035f637f3cbb85afee9f9d319631580e7621cf790cd16ca063a2f820e",
))
.header(ContentType::JSON)
.remote("127.0.0.1:8000".parse().unwrap())
.body(r#"{ "secret": "valid" }"#)
.dispatch();
assert_eq!(response.status(), Status::Ok);
let response = client
.post("/")
.header(ContentType::JSON)
.remote("127.0.0.1:8000".parse().unwrap())
.body(r#"{ "secret": "invalid" }"#)
.dispatch();
assert_eq!(response.status(), Status::Unauthorized);
let response = client
.post("/")
.header(ContentType::JSON)
.remote("127.0.0.1:8000".parse().unwrap())
.body(r#"{ "not_secret": "invalid" }"#)
.body(&serde_json::to_string(&json!({ "foo": "bar" })).unwrap())
.dispatch();
assert_eq!(response.status(), Status::NotFound);
let response = client
.post("/")
.header(Header::new("X-Gitea-Signature", "beef"))
.header(ContentType::JSON)
.remote("127.0.0.1:8000".parse().unwrap())
.body(&serde_json::to_string(&json!({ "foo": "bar" })).unwrap())
.dispatch();
assert_eq!(response.status(), Status::Unauthorized);
let response = client
.post("/")
.header(Header::new(
"X-Gitea-Signature",
"c5c315d76318362ec129ca629b50b626bba09ad3d7ba4cc0f4c0afe4a90537a0",
))
.header(ContentType::JSON)
.remote("127.0.0.1:8000".parse().unwrap())
.body(r#"{ "not_secret": "invalid" "#)
@ -283,44 +431,48 @@ mod tests {
#[test]
fn parse_command() {
let mut map = HeaderMap::new();
map.add_raw("X-Gitea-Event", "something");
assert_eq!(
replace_parameter("command", &serde_json::Value::Null).unwrap(),
replace_parameter("command", &map, &serde_json::Value::Null).unwrap(),
"command"
);
assert_eq!(
replace_parameter(" command", &serde_json::Value::Null).unwrap(),
replace_parameter(" command", &map, &serde_json::Value::Null).unwrap(),
" command"
);
assert_eq!(
replace_parameter("command ", &serde_json::Value::Null).unwrap(),
replace_parameter("command ", &map, &serde_json::Value::Null).unwrap(),
"command "
);
assert_eq!(
replace_parameter(" command ", &serde_json::Value::Null).unwrap(),
replace_parameter(" command ", &map, &serde_json::Value::Null).unwrap(),
" command "
);
assert_eq!(
replace_parameter("command command ", &serde_json::Value::Null).unwrap(),
replace_parameter("command command ", &map, &serde_json::Value::Null).unwrap(),
"command command "
);
assert_eq!(
replace_parameter("{{ /foo }} command", &json!({ "foo": "bar" })).unwrap(),
replace_parameter("{{ /foo }} command", &map, &json!({ "foo": "bar" })).unwrap(),
"bar command"
);
assert_eq!(
replace_parameter(" command {{ /foo }} ", &json!({ "foo": "bar" })).unwrap(),
replace_parameter(" command {{ /foo }} ", &map, &json!({ "foo": "bar" })).unwrap(),
" command bar "
);
assert_eq!(
replace_parameter(
"{{ /foo }} command{{/field1/foo}}",
&map,
&json!({ "foo": "bar", "field1": { "foo": "baz" } })
)
.unwrap(),
@ -328,17 +480,20 @@ mod tests {
);
assert_eq!(
replace_parameter(" command {{ /foo }} ", &json!({ "foo": "bar" })).unwrap(),
replace_parameter(" command {{ /foo }} ", &map, &json!({ "foo": "bar" })).unwrap(),
" command bar "
);
assert_eq!(
replace_parameter(
" {{ /field1/foo }} command",
&map,
&json!({ "field1": { "foo": "bar" } })
)
.unwrap(),
" bar command"
);
// Add tests with header fields
}
}