Restructure and minor improvements
In order to keep things together the code was restructured. Some small improvements such as clippy warnings and the validation of a hook in regards of the ip filter.
This commit is contained in:
parent
d29bfdf88d
commit
65430e65b7
4 changed files with 169 additions and 169 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -1242,7 +1242,7 @@ checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
|
|||
|
||||
[[package]]
|
||||
name = "webhookey"
|
||||
version = "0.1.0-rc.2"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "webhookey"
|
||||
version = "0.1.0-rc.2"
|
||||
version = "0.1.0"
|
||||
authors = ["finga <webhookey@onders.org>"]
|
||||
edition = "2018"
|
||||
license = "GPL-3.0-or-later"
|
||||
|
|
18
README.md
18
README.md
|
@ -67,12 +67,12 @@ Whereas `<config_dir>` depends on the platform:
|
|||
#### Hooks
|
||||
With `hooks` you can configure a sequence of hooks. A single hook
|
||||
consists of the following fields:
|
||||
- command: A command to be executed if a filter matches
|
||||
- allow/deny: An optional parameter to either allow or deny specific
|
||||
source addresses or ranges.
|
||||
- signature: Name of the HTTP header field containing the signature.
|
||||
- secrets: List of secrets.
|
||||
- filter: Tree of filters.
|
||||
- `command`: A command to be executed if a filter matches
|
||||
- `allow`/`deny`: An optional parameter to either allow or deny
|
||||
specific source addresses or ranges.
|
||||
- `signature`: Name of the HTTP header field containing the signature.
|
||||
- `secrets`: List of secrets.
|
||||
- `filter`: Tree of filters.
|
||||
|
||||
Example:
|
||||
```yaml
|
||||
|
@ -163,7 +163,7 @@ Conjunction filters contain lists of other filters.
|
|||
The `json` filter matches a regular expression on a field from the
|
||||
received JSON data.
|
||||
|
||||
- pointer: Pointer to the JSON field according to [RFC
|
||||
- `pointer`: Pointer to the JSON field according to [RFC
|
||||
6901](https://tools.ietf.org/html/rfc6901).
|
||||
- regex: Regular expression which has to match the field pointed to
|
||||
by the pointer.
|
||||
- `regex`: Regular expression which has to match the field pointed
|
||||
to by the pointer.
|
||||
|
|
316
src/main.rs
316
src/main.rs
|
@ -34,6 +34,26 @@ use std::{
|
|||
net::{IpAddr, Ipv4Addr, SocketAddr},
|
||||
};
|
||||
|
||||
#[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),
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(deny_unknown_fields, untagged)]
|
||||
enum AddrType {
|
||||
|
@ -66,12 +86,6 @@ impl IpFilter {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct Config {
|
||||
hooks: HashMap<String, Hook>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct JsonFilter {
|
||||
|
@ -171,24 +185,27 @@ struct Hook {
|
|||
filter: FilterType,
|
||||
}
|
||||
|
||||
#[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),
|
||||
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)?);
|
||||
}
|
||||
}
|
||||
|
||||
replace_parameters(&self.command, &request.headers(), data)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -196,27 +213,128 @@ struct Hooks {
|
|||
inner: HashMap<String, String>,
|
||||
}
|
||||
|
||||
fn accept_ip(hook_name: &str, client_ip: &IpAddr, ip_filter: &Option<IpFilter>) -> bool {
|
||||
match ip_filter {
|
||||
Some(filter) => {
|
||||
if filter.validate(client_ip) {
|
||||
info!("Allow hook `{}` from {}", &hook_name, &client_ip);
|
||||
true
|
||||
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 {
|
||||
return accept_ip(&name, &client_ip, &ip);
|
||||
} else {
|
||||
warn!("Deny hook `{}` from {}", &hook_name, &client_ip);
|
||||
false
|
||||
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()
|
||||
.map(|secret| validate_request(&secret, &signature, &buffer));
|
||||
|
||||
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) {
|
||||
Ok(true) => match hook.get_command(&hook_name, &request, &mut data) {
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
info!(
|
||||
"Allow hook `{}` from {}, no IP filter was configured",
|
||||
&hook_name, &client_ip
|
||||
);
|
||||
true
|
||||
|
||||
if !valid {
|
||||
return Err(WebhookeyError::Unauthorized(*client_ip));
|
||||
}
|
||||
|
||||
Ok(Hooks { inner: result })
|
||||
}
|
||||
}
|
||||
|
||||
impl FromDataSimple for Hooks {
|
||||
type Error = WebhookeyError;
|
||||
|
||||
fn from_data(request: &Request, data: Data) -> data::Outcome<Self, Self::Error> {
|
||||
match Hooks::get_commands(&request, data) {
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct Config {
|
||||
hooks: HashMap<String, Hook>,
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
fn validate_request(secret: &str, signature: &str, data: &[u8]) -> Result<()> {
|
||||
let mut mac = Hmac::<Sha256>::new_varkey(&secret.as_bytes())
|
||||
.map_err(|e| anyhow!("Could not create hasher with secret: {}", e))?;
|
||||
|
@ -299,124 +417,6 @@ fn get_string(value: &serde_json::Value) -> Result<String, WebhookeyError> {
|
|||
}
|
||||
}
|
||||
|
||||
fn get_command(
|
||||
hook_name: &str,
|
||||
hook: &Hook,
|
||||
request: &Request,
|
||||
data: &mut serde_json::Value,
|
||||
) -> Result<String> {
|
||||
trace!("Replacing parameters for command of hook `{}`", hook_name);
|
||||
|
||||
for parameter in get_parameter(&hook.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)?);
|
||||
}
|
||||
}
|
||||
|
||||
replace_parameters(&hook.command, &request.headers(), data)
|
||||
}
|
||||
|
||||
fn get_commands(request: &Request, data: Data) -> Result<Hooks, 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)| accept_ip(&name, &client_ip, &hook.ip_filter));
|
||||
|
||||
for (hook_name, hook) in hooks {
|
||||
let signature = request
|
||||
.headers()
|
||||
.get_one(&hook.signature)
|
||||
.ok_or(WebhookeyError::InvalidSignature)?;
|
||||
|
||||
let secrets = hook
|
||||
.secrets
|
||||
.iter()
|
||||
.map(|secret| validate_request(&secret, &signature, &buffer));
|
||||
|
||||
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) {
|
||||
Ok(true) => match get_command(&hook_name, &hook, &request, &mut data) {
|
||||
Ok(command) => {
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !valid {
|
||||
return Err(WebhookeyError::Unauthorized(*client_ip));
|
||||
}
|
||||
|
||||
Ok(Hooks { inner: result })
|
||||
}
|
||||
|
||||
impl FromDataSimple for Hooks {
|
||||
type Error = WebhookeyError;
|
||||
|
||||
fn from_data(request: &Request, data: Data) -> data::Outcome<Self, Self::Error> {
|
||||
match get_commands(&request, data) {
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/", format = "json", data = "<hooks>")]
|
||||
fn receive_hook<'a>(address: SocketAddr, hooks: Hooks) -> Result<Response<'a>> {
|
||||
info!("Post request received from: {}", address);
|
||||
|
@ -486,7 +486,7 @@ fn main() -> Result<()> {
|
|||
)
|
||||
.subcommand(
|
||||
App::new("configtest")
|
||||
.about("Verifies if the configuration can be parsed without errors."),
|
||||
.about("Verifies if the configuration can be parsed without errors"),
|
||||
)
|
||||
.get_matches();
|
||||
|
||||
|
@ -500,7 +500,7 @@ fn main() -> Result<()> {
|
|||
if let Some(_) = cli.subcommand_matches("configtest") {
|
||||
debug!("Configtest succeded.");
|
||||
println!("Config is OK");
|
||||
return Ok(())
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
rocket::ignite()
|
||||
|
|
Loading…
Reference in a new issue