Add optional ip_filter to hook config

In order to allow or deny sources of requests the possibility to
configure a list of allowed or denied IP addresses was added as
described by the readme.

Closes #3
This commit is contained in:
finga 2021-04-02 00:25:39 +02:00
parent 8099bf773f
commit 8314214e06
5 changed files with 239 additions and 96 deletions

View file

@ -2,6 +2,7 @@
use anyhow::{anyhow, bail, Result};
use hmac::{Hmac, Mac, NewMac};
use ipnet::IpNet;
use log::{debug, error, info, trace, warn};
use nom::{
branch::alt,
@ -28,25 +29,43 @@ use std::{
collections::HashMap,
fs::File,
io::{BufReader, Read},
net::SocketAddr,
net::{IpAddr, Ipv4Addr, SocketAddr},
process::Command,
str::from_utf8,
};
#[derive(Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields, untagged)]
enum AddrType {
IpAddr(IpAddr),
IpNet(IpNet),
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields, rename_all = "lowercase")]
enum IpFilter {
Allow(Vec<AddrType>),
Deny(Vec<AddrType>),
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
struct Config {
hooks: HashMap<String, Hook>,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
struct Hook {
command: String,
signature: String,
ip_filter: Option<IpFilter>,
secrets: Vec<String>,
filters: HashMap<String, Filter>,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
struct Filter {
pointer: String,
regex: String,
@ -55,6 +74,58 @@ struct Filter {
#[derive(Debug)]
struct Hooks(HashMap<String, String>);
fn accepted_ip(hook_name: &str, client_ip: &IpAddr, ip_filter: &Option<IpFilter>) -> bool {
match ip_filter {
Some(IpFilter::Allow(list)) => {
for i in list {
match i {
AddrType::IpAddr(addr) => {
if addr == client_ip {
info!("Allow hook `{}` from {}", &hook_name, &addr);
return true;
}
}
AddrType::IpNet(net) => {
if net.contains(client_ip) {
info!("Allow hook `{}` from {}", &hook_name, &net);
return true;
}
}
}
}
warn!("Deny hook `{}` from {}", &hook_name, &client_ip);
return false;
}
Some(IpFilter::Deny(list)) => {
for i in list {
match i {
AddrType::IpAddr(addr) => {
if addr == client_ip {
warn!("Deny hook `{}` from {}", &hook_name, &addr);
return false;
}
}
AddrType::IpNet(net) => {
if net.contains(client_ip) {
warn!("Deny hook `{}` from {}", &hook_name, &net);
return false;
}
}
}
}
info!("Allow hook `{}` from {}", &hook_name, &client_ip)
}
None => info!(
"Allow hook `{}` from {} as no IP filter was configured",
&hook_name, &client_ip
),
}
true
}
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))?;
@ -163,75 +234,77 @@ impl FromDataSimple for Hooks {
let config = request.guard::<State<Config>>().unwrap(); // should never fail
let mut valid = false;
let mut hooks = HashMap::new();
let client_ip = &request
.client_ip()
.unwrap_or(IpAddr::V4(Ipv4Addr::UNSPECIFIED));
for (hook_name, hook) in &config.hooks {
if let Some(signature) = request.headers().get_one(&hook.signature) {
for secret in &hook.secrets {
match validate_request(&secret, &signature, &buffer) {
Ok(()) => {
trace!("Valid signature found for hook `{}`", hook_name,);
if accepted_ip(&hook_name, &client_ip, &hook.ip_filter) {
if let Some(signature) = request.headers().get_one(&hook.signature) {
for secret in &hook.secrets {
match validate_request(&secret, &signature, &buffer) {
Ok(()) => {
trace!("Valid signature found for hook `{}`", hook_name,);
valid = true;
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 {
match filter_match(
&hook_name,
&hook,
&filter_name,
&filter,
&request,
&data,
) {
Ok(Some(command)) => {
hooks.insert(hook_name.to_string(), command);
break;
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 {
match filter_match(
&hook_name,
&hook,
&filter_name,
&filter,
&request,
&data,
) {
Ok(Some(command)) => {
hooks.insert(hook_name.to_string(), command);
break;
}
Ok(None) => {}
Err(e) => error!("{}", e),
}
Ok(None) => {}
Err(e) => error!("{}", e),
}
}
}
Err(e) => {
error!("Could not validate request: {}", e);
return Failure((
Status::Unauthorized,
anyhow!("Could not validate request: {}", e),
));
Err(e) => {
warn!("Hook `{}` could not validate request: {}", &hook_name, e);
}
}
}
} else {
error!("Could not extract signature from header");
return Failure((
Status::BadRequest,
anyhow!("Could not extract signature from header"),
));
}
} else {
error!("Could not extract signature from header");
return Failure((
Status::BadRequest,
anyhow!("Could not extract signature from header"),
));
}
}
if hooks.is_empty() {
if valid {
warn!("Unmatched hook from {:?}", &request.client_ip());
warn!("Unmatched hook from {}", &client_ip);
return Failure((
Status::NotFound,
anyhow!("Unmatched hook from {:?}", &request.client_ip()),
anyhow!("Unmatched hook from {}", &client_ip),
));
} else {
error!("Unauthorized request from {:?}", &request.client_ip());
error!("Unauthorized request from {}", &client_ip);
return Failure((
Status::Unauthorized,
anyhow!("Unauthorized request from {:?}", &request.client_ip()),
anyhow!("Unauthorized request from {}", &client_ip),
));
}
}
@ -353,6 +426,7 @@ mod tests {
Hook {
command: "".to_string(),
signature: "X-Gitea-Signature".to_string(),
ip_filter: None,
secrets: vec!["valid".to_string()],
filters: HashMap::new(),
},