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 = [ dependencies = [
"hermit-abi", "hermit-abi",
"libc", "libc",
"winapi 0.3.9", "winapi",
] ]
[[package]] [[package]]
@ -172,12 +172,6 @@ version = "1.0.67"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd" checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd"
[[package]]
name = "cfg-if"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
version = "1.0.0" version = "1.0.0"
@ -234,7 +228,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7e9d99fa91428effe99c5c6d4634cdeba32b8cf784fc428a2a687f61a952c49" checksum = "e7e9d99fa91428effe99c5c6d4634cdeba32b8cf784fc428a2a687f61a952c49"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"cfg-if 1.0.0", "cfg-if",
"lazy_static", "lazy_static",
] ]
@ -315,7 +309,7 @@ checksum = "8e93d7f5705de3e49895a2b5e0b8855a1c27f080192ae9c32a6432d50741a57a"
dependencies = [ dependencies = [
"libc", "libc",
"redox_users", "redox_users",
"winapi 0.3.9", "winapi",
] ]
[[package]] [[package]]
@ -337,53 +331,6 @@ dependencies = [
"termcolor", "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]] [[package]]
name = "funty" name = "funty"
version = "1.1.0" version = "1.1.0"
@ -406,7 +353,7 @@ version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
dependencies = [ dependencies = [
"cfg-if 1.0.0", "cfg-if",
"libc", "libc",
"wasi 0.9.0+wasi-snapshot-preview1", "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" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9495705279e7140bf035dde1f6e750c162df8b625267cd52cc44e0b156732c8" checksum = "c9495705279e7140bf035dde1f6e750c162df8b625267cd52cc44e0b156732c8"
dependencies = [ dependencies = [
"cfg-if 1.0.0", "cfg-if",
"libc", "libc",
"wasi 0.10.2+wasi-snapshot-preview1", "wasi 0.10.2+wasi-snapshot-preview1",
] ]
@ -453,6 +400,12 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]] [[package]]
name = "hkdf" name = "hkdf"
version = "0.10.0" version = "0.10.0"
@ -537,51 +490,12 @@ dependencies = [
"hashbrown", "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]] [[package]]
name = "itoa" name = "itoa"
version = "0.4.7" version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" 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]] [[package]]
name = "language-tags" name = "language-tags"
version = "0.2.2" version = "0.2.2"
@ -594,12 +508,6 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "lazycell"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
[[package]] [[package]]
name = "lexical-core" name = "lexical-core"
version = "0.7.5" version = "0.7.5"
@ -608,7 +516,7 @@ checksum = "21f866863575d0e1d654fbeeabdc927292fdf862873dc3c96c6f753357e13374"
dependencies = [ dependencies = [
"arrayvec", "arrayvec",
"bitflags", "bitflags",
"cfg-if 1.0.0", "cfg-if",
"ryu", "ryu",
"static_assertions", "static_assertions",
] ]
@ -640,7 +548,7 @@ version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
dependencies = [ dependencies = [
"cfg-if 1.0.0", "cfg-if",
] ]
[[package]] [[package]]
@ -664,60 +572,6 @@ dependencies = [
"log 0.3.9", "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]] [[package]]
name = "nom" name = "nom"
version = "6.1.2" version = "6.1.2"
@ -731,24 +585,6 @@ dependencies = [
"version_check 0.9.3", "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]] [[package]]
name = "num_cpus" name = "num_cpus"
version = "1.13.0" version = "1.13.0"
@ -904,15 +740,6 @@ version = "0.1.57"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" 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]] [[package]]
name = "redox_users" name = "redox_users"
version = "0.3.5" version = "0.3.5"
@ -920,7 +747,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d" checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d"
dependencies = [ dependencies = [
"getrandom 0.1.16", "getrandom 0.1.16",
"redox_syscall 0.1.57", "redox_syscall",
"rust-argon2", "rust-argon2",
] ]
@ -989,19 +816,6 @@ dependencies = [
"yansi", "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]] [[package]]
name = "rocket_http" name = "rocket_http"
version = "0.4.7" version = "0.4.7"
@ -1059,15 +873,6 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" 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]] [[package]]
name = "sct" name = "sct"
version = "0.4.0" version = "0.4.0"
@ -1128,18 +933,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa827a14b29ab7f44778d14a88d3cb76e949c45083f7dbfa507d0cb699dc12de" checksum = "fa827a14b29ab7f44778d14a88d3cb76e949c45083f7dbfa507d0cb699dc12de"
dependencies = [ dependencies = [
"block-buffer", "block-buffer",
"cfg-if 1.0.0", "cfg-if",
"cpuid-bool 0.1.2", "cpuid-bool 0.1.2",
"digest", "digest",
"opaque-debug", "opaque-debug",
] ]
[[package]]
name = "slab"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8"
[[package]] [[package]]
name = "smallvec" name = "smallvec"
version = "1.6.1" version = "1.6.1"
@ -1208,7 +1007,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438"
dependencies = [ dependencies = [
"libc", "libc",
"winapi 0.3.9", "winapi",
] ]
[[package]] [[package]]
@ -1331,17 +1130,6 @@ version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" 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]] [[package]]
name = "wasi" name = "wasi"
version = "0.9.0+wasi-snapshot-preview1" version = "0.9.0+wasi-snapshot-preview1"
@ -1361,14 +1149,16 @@ dependencies = [
"anyhow", "anyhow",
"dirs", "dirs",
"env_logger", "env_logger",
"hex",
"hmac",
"log 0.4.14", "log 0.4.14",
"nom", "nom",
"regex", "regex",
"rocket", "rocket",
"rocket_contrib",
"serde", "serde",
"serde_json", "serde_json",
"serde_yaml", "serde_yaml",
"sha2",
] ]
[[package]] [[package]]
@ -1391,12 +1181,6 @@ dependencies = [
"webpki", "webpki",
] ]
[[package]]
name = "winapi"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a"
[[package]] [[package]]
name = "winapi" name = "winapi"
version = "0.3.9" version = "0.3.9"
@ -1407,12 +1191,6 @@ dependencies = [
"winapi-x86_64-pc-windows-gnu", "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]] [[package]]
name = "winapi-i686-pc-windows-gnu" name = "winapi-i686-pc-windows-gnu"
version = "0.4.0" version = "0.4.0"
@ -1425,7 +1203,7 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
dependencies = [ dependencies = [
"winapi 0.3.9", "winapi",
] ]
[[package]] [[package]]
@ -1434,16 +1212,6 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 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]] [[package]]
name = "wyz" name = "wyz"
version = "0.2.0" version = "0.2.0"

View file

@ -12,7 +12,6 @@ tls = ["rocket/tls"]
[dependencies] [dependencies]
rocket = "0.4" rocket = "0.4"
rocket_contrib = { version = "0.4", default-features = false, features = ["json"] }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
serde_yaml = "0.8" serde_yaml = "0.8"
@ -22,3 +21,6 @@ anyhow = "1.0"
log = "0.4" log = "0.4"
env_logger = "0.8" env_logger = "0.8"
nom = "6" 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 Right now there is only the configuration parameter for hooks, here
each hook has to be configured, It contains following fields: each hook has to be configured, It contains following fields:
- command: Optional string for a command to be executed when all - command: String for a command to be executed when all filters
filters match. Pointers ([RFC match. Pointers ([RFC 6901](https://tools.ietf.org/html/rfc6901)) to
6901](https://tools.ietf.org/html/rfc6901)) to JSON fields may be JSON fields may be used to be replaced with data from the JSON data
used to be replaced with data from the JSON data with `{{ with `{{ /field/pointed/to }}`. Further `{{ event }}` and `{{
/field/pointed/to }}` signature }}` are valid variables as they contain the values from
the regarding header fields of the http request.
- secrets: List of secrets. - secrets: List of secrets.
- filters: List of filters. - filters: List of filters.
@ -81,7 +82,6 @@ Whereas `<config_dir>` depends on the platform:
- Windows: `{FOLDERID_RoamingAppData}` - Windows: `{FOLDERID_RoamingAppData}`
# TODOs # TODOs
## Use `lazy_static` or `once_cell` for compiled regexes
## Use `clap` to parse command line arguments ## Use `clap` to parse command line arguments
## Implement the functionality to reply to certain webhooks ## Implement the functionality to reply to certain webhooks
## Configure rocket via config.yml ## Configure rocket via config.yml

View file

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