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]]
|
[[package]]
|
||||||
name = "webhookey"
|
name = "webhookey"
|
||||||
version = "0.1.0-rc.2"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "webhookey"
|
name = "webhookey"
|
||||||
version = "0.1.0-rc.2"
|
version = "0.1.0"
|
||||||
authors = ["finga <webhookey@onders.org>"]
|
authors = ["finga <webhookey@onders.org>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
license = "GPL-3.0-or-later"
|
license = "GPL-3.0-or-later"
|
||||||
|
|
18
README.md
18
README.md
|
@ -67,12 +67,12 @@ Whereas `<config_dir>` depends on the platform:
|
||||||
#### Hooks
|
#### Hooks
|
||||||
With `hooks` you can configure a sequence of hooks. A single hook
|
With `hooks` you can configure a sequence of hooks. A single hook
|
||||||
consists of the following fields:
|
consists of the following fields:
|
||||||
- command: A command to be executed if a filter matches
|
- `command`: A command to be executed if a filter matches
|
||||||
- allow/deny: An optional parameter to either allow or deny specific
|
- `allow`/`deny`: An optional parameter to either allow or deny
|
||||||
source addresses or ranges.
|
specific source addresses or ranges.
|
||||||
- signature: Name of the HTTP header field containing the signature.
|
- `signature`: Name of the HTTP header field containing the signature.
|
||||||
- secrets: List of secrets.
|
- `secrets`: List of secrets.
|
||||||
- filter: Tree of filters.
|
- `filter`: Tree of filters.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
```yaml
|
```yaml
|
||||||
|
@ -163,7 +163,7 @@ Conjunction filters contain lists of other filters.
|
||||||
The `json` filter matches a regular expression on a field from the
|
The `json` filter matches a regular expression on a field from the
|
||||||
received JSON data.
|
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).
|
6901](https://tools.ietf.org/html/rfc6901).
|
||||||
- regex: Regular expression which has to match the field pointed to
|
- `regex`: Regular expression which has to match the field pointed
|
||||||
by the pointer.
|
to by the pointer.
|
||||||
|
|
312
src/main.rs
312
src/main.rs
|
@ -34,6 +34,26 @@ use std::{
|
||||||
net::{IpAddr, Ipv4Addr, SocketAddr},
|
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)]
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
#[serde(deny_unknown_fields, untagged)]
|
#[serde(deny_unknown_fields, untagged)]
|
||||||
enum AddrType {
|
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)]
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
struct JsonFilter {
|
struct JsonFilter {
|
||||||
|
@ -171,24 +185,27 @@ struct Hook {
|
||||||
filter: FilterType,
|
filter: FilterType,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
impl Hook {
|
||||||
enum WebhookeyError {
|
fn get_command(
|
||||||
#[error("Could not extract signature from header")]
|
&self,
|
||||||
InvalidSignature,
|
hook_name: &str,
|
||||||
#[error("Unauthorized request from `{0}`")]
|
request: &Request,
|
||||||
Unauthorized(IpAddr),
|
data: &mut serde_json::Value,
|
||||||
#[error("Unmatched hook from `{0}`")]
|
) -> Result<String> {
|
||||||
UnmatchedHook(IpAddr),
|
trace!("Replacing parameters for command of hook `{}`", hook_name);
|
||||||
#[error("Could not find field refered to in parameter `{0}`")]
|
|
||||||
InvalidParameterPointer(String),
|
for parameter in get_parameter(&self.command)? {
|
||||||
#[error("Could not evaluate filter request")]
|
let parameter = parameter.trim();
|
||||||
InvalidFilter,
|
|
||||||
#[error("IO Error")]
|
if let Some(json_value) = data.pointer(parameter) {
|
||||||
Io(std::io::Error),
|
*data.pointer_mut(parameter).ok_or_else(|| {
|
||||||
#[error("Serde Error")]
|
WebhookeyError::InvalidParameterPointer(parameter.to_string())
|
||||||
Serde(serde_json::Error),
|
})? = serde_json::Value::String(get_string(json_value)?);
|
||||||
#[error("Regex Error")]
|
}
|
||||||
Regex(regex::Error),
|
}
|
||||||
|
|
||||||
|
replace_parameters(&self.command, &request.headers(), data)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -196,25 +213,126 @@ struct Hooks {
|
||||||
inner: HashMap<String, String>,
|
inner: HashMap<String, String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn accept_ip(hook_name: &str, client_ip: &IpAddr, ip_filter: &Option<IpFilter>) -> bool {
|
impl Hooks {
|
||||||
match ip_filter {
|
fn get_commands(request: &Request, data: Data) -> Result<Self, WebhookeyError> {
|
||||||
Some(filter) => {
|
let mut buffer = Vec::new();
|
||||||
if filter.validate(client_ip) {
|
let size = data
|
||||||
info!("Allow hook `{}` from {}", &hook_name, &client_ip);
|
.open()
|
||||||
true
|
.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 {
|
} else {
|
||||||
warn!("Deny hook `{}` from {}", &hook_name, &client_ip);
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
info!(
|
info!(
|
||||||
"Allow hook `{}` from {}, no IP filter was configured",
|
"Allow hook `{}` from {}, no IP filter was configured",
|
||||||
&hook_name, &client_ip
|
&name, &client_ip
|
||||||
);
|
);
|
||||||
true
|
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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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<()> {
|
fn validate_request(secret: &str, signature: &str, data: &[u8]) -> Result<()> {
|
||||||
|
@ -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>")]
|
#[post("/", format = "json", data = "<hooks>")]
|
||||||
fn receive_hook<'a>(address: SocketAddr, hooks: Hooks) -> Result<Response<'a>> {
|
fn receive_hook<'a>(address: SocketAddr, hooks: Hooks) -> Result<Response<'a>> {
|
||||||
info!("Post request received from: {}", address);
|
info!("Post request received from: {}", address);
|
||||||
|
@ -486,7 +486,7 @@ fn main() -> Result<()> {
|
||||||
)
|
)
|
||||||
.subcommand(
|
.subcommand(
|
||||||
App::new("configtest")
|
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();
|
.get_matches();
|
||||||
|
|
||||||
|
@ -500,7 +500,7 @@ fn main() -> Result<()> {
|
||||||
if let Some(_) = cli.subcommand_matches("configtest") {
|
if let Some(_) = cli.subcommand_matches("configtest") {
|
||||||
debug!("Configtest succeded.");
|
debug!("Configtest succeded.");
|
||||||
println!("Config is OK");
|
println!("Config is OK");
|
||||||
return Ok(())
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
rocket::ignite()
|
rocket::ignite()
|
||||||
|
|
Loading…
Reference in a new issue