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:
parent
8099bf773f
commit
8314214e06
5 changed files with 239 additions and 96 deletions
172
src/main.rs
172
src/main.rs
|
@ -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(),
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue