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
let mut hooks = HashMap::new();
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),
));
}
}
trace!("Data received: {:?}", from_utf8(&buffer));
let mut valid = false;
for (hook_name, hook) in &config.hooks {
let mut commands = Vec::new();
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),
));
}
};
mac.update(&buffer);
match &hex::decode(&signature.as_bytes()) {
Ok(raw_signature) => {
if mac.verify(&raw_signature) == Ok(()) {
trace!(
"Valid signature found for hook `{}`: {}",
hook_name,
signature
);
valid = true;
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),
));
}
};
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) = data.pointer(&filter.pointer) {
let regex = Regex::new(&filter.regex)?;
if let Some(value) = value.as_str() { if let Some(value) = value.as_str() {
if !regex.is_match(value) { if regex.is_match(value) {
info!("Filter `{}` in hook `{}` did not match", filter_name, name); debug!(
return Ok(()); "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 { } else {
anyhow!( anyhow!(
"Could not parse pointer in hook `{}` from filter `{}`", "Could not parse pointer in hook `{}` from filter `{}`",
name, hook_name,
filter_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 let Some(command) = &hook.command { if !commands.is_empty() {
let command = replace_parameter(&command, data)?; hooks.insert(hook_name.to_string(), commands);
}
}
info!("Execute `{}` from hook `{}`", command, name); if hooks.is_empty() {
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 {
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>>(); let command = command.split(' ').collect::<Vec<&str>>();
let exec_command = Command::new(&command[0]).args(&command[1..]).output()?; match Command::new(&command[0]).args(&command[1..]).output() {
Ok(executed) => {
info!( info!(
"Command `{}` exited with return code: {}", "Command `{}` exited with return code: {}",
&command[0], &exec_command.status &command[0], &executed.status
); );
trace!( trace!(
"Output of command `{}` on stdout: {:?}", "Output of command `{}` on stdout: {:?}",
&command[0], &command[0],
from_utf8(&exec_command.stdout)? from_utf8(&executed.stdout)?
); );
debug!( debug!(
"Output of command `{}` on stderr: {:?}", "Output of command `{}` on stderr: {:?}",
&command[0], &command[0],
from_utf8(&exec_command.stderr)? from_utf8(&executed.stderr)?
); );
} }
Err(e) => {
Ok(()) error!("Execution of `{}` failed: {}", command[0], e);
}
#[post("/", format = "json", data = "<data>")]
fn receive_hook(address: SocketAddr, config: State<Config>, data: Json<Data>) -> Result<Response> {
info!("Post request received from: {}", address);
let mut response = Response::new();
let data = serde_json::to_value(data.0)?;
trace!("Data received from: {}\n{}", address, data);
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();
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)?;
} }
} }
} else {
warn!("Data received from {} contains invalid data", address);
response.set_status(Status::BadRequest);
} }
} 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
} }
} }