Update build deps and improve readability
The ip filtering is improved as well as the replacing of parameters. The index page is removed.
This commit is contained in:
parent
29caaff596
commit
891a8a70ae
4 changed files with 204 additions and 280 deletions
278
src/main.rs
278
src/main.rs
|
@ -16,7 +16,6 @@ use regex::Regex;
|
|||
use rocket::{
|
||||
data::{self, FromDataSimple},
|
||||
fairing::AdHoc,
|
||||
get,
|
||||
http::{HeaderMap, Status},
|
||||
post, routes, Data,
|
||||
Outcome::{Failure, Success},
|
||||
|
@ -41,6 +40,15 @@ enum AddrType {
|
|||
IpNet(IpNet),
|
||||
}
|
||||
|
||||
impl AddrType {
|
||||
fn matches(&self, client_ip: &IpAddr) -> bool {
|
||||
match self {
|
||||
AddrType::IpAddr(addr) => addr == client_ip,
|
||||
AddrType::IpNet(net) => net.contains(client_ip),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(deny_unknown_fields, rename_all = "lowercase")]
|
||||
enum IpFilter {
|
||||
|
@ -48,6 +56,15 @@ enum IpFilter {
|
|||
Deny(Vec<AddrType>),
|
||||
}
|
||||
|
||||
impl IpFilter {
|
||||
fn validate(&self, client_ip: &IpAddr) -> bool {
|
||||
match self {
|
||||
IpFilter::Allow(list) => list.iter().any(|i| i.matches(client_ip)),
|
||||
IpFilter::Deny(list) => !list.iter().any(|i| i.matches(client_ip)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct Config {
|
||||
|
@ -74,7 +91,7 @@ struct Filter {
|
|||
#[derive(Debug, Error)]
|
||||
enum WebhookeyError {
|
||||
#[error("Could not extract signature from header")]
|
||||
InvalidHeader,
|
||||
InvalidSignature,
|
||||
#[error("Unauthorized request from `{0}`")]
|
||||
Unauthorized(IpAddr),
|
||||
#[error("Unmatched hook from `{0}`")]
|
||||
|
@ -86,58 +103,29 @@ enum WebhookeyError {
|
|||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Hooks(HashMap<String, String>);
|
||||
struct Hooks {
|
||||
inner: HashMap<String, String>,
|
||||
}
|
||||
|
||||
fn accept_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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(filter) => {
|
||||
if filter.validate(client_ip) {
|
||||
info!("Allow hook `{}` from {}", &hook_name, &client_ip);
|
||||
true
|
||||
} else {
|
||||
warn!("Deny hook `{}` from {}", &hook_name, &client_ip);
|
||||
false
|
||||
}
|
||||
|
||||
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 {}, no IP filter was configured",
|
||||
&hook_name, &client_ip
|
||||
);
|
||||
true
|
||||
}
|
||||
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<()> {
|
||||
|
@ -161,7 +149,31 @@ fn get_parameter(input: &str) -> Result<Vec<&str>> {
|
|||
Ok(result)
|
||||
}
|
||||
|
||||
fn replace_parameter(input: &str, headers: &HeaderMap, data: &serde_json::Value) -> Result<String> {
|
||||
fn get_header_field<'a>(headers: &'a HeaderMap, param: &[&str]) -> Result<&'a str> {
|
||||
headers
|
||||
.get_one(
|
||||
param
|
||||
.get(1)
|
||||
.ok_or_else(|| anyhow!("Missing parameter for `header` expression"))?,
|
||||
)
|
||||
.ok_or_else(|| anyhow!("Could not extract event parameter from header"))
|
||||
}
|
||||
|
||||
fn get_value_from_pointer<'a>(data: &'a serde_json::Value, pointer: &'a str) -> Result<&'a str> {
|
||||
let value = data
|
||||
.pointer(pointer)
|
||||
.ok_or_else(|| anyhow!("Could not get field from pointer {}", pointer))?;
|
||||
|
||||
value
|
||||
.as_str()
|
||||
.ok_or_else(|| anyhow!("Could not convert value `{}` to string", value))
|
||||
}
|
||||
|
||||
fn replace_parameters(
|
||||
input: &str,
|
||||
headers: &HeaderMap,
|
||||
data: &serde_json::Value,
|
||||
) -> Result<String> {
|
||||
let parse: IResult<&str, Vec<&str>> = many0(alt((
|
||||
map_res(
|
||||
delimited(tag("{{"), take_until("}}"), tag("}}")),
|
||||
|
@ -169,23 +181,8 @@ fn replace_parameter(input: &str, headers: &HeaderMap, data: &serde_json::Value)
|
|||
let expr = param.trim().split(' ').collect::<Vec<&str>>();
|
||||
|
||||
match expr.get(0) {
|
||||
Some(&"header") => {
|
||||
if let Some(field) = expr.get(1) {
|
||||
match headers.get_one(field) {
|
||||
Some(value) => Ok(value),
|
||||
_ => bail!("Could not extract event parameter from header"),
|
||||
}
|
||||
} else {
|
||||
bail!("Missing parameter for `header` expression");
|
||||
}
|
||||
}
|
||||
Some(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()),
|
||||
},
|
||||
Some(&"header") => get_header_field(headers, &expr),
|
||||
Some(pointer) => get_value_from_pointer(data, &pointer),
|
||||
None => bail!("Missing expression in `{}`", input),
|
||||
}
|
||||
},
|
||||
|
@ -203,12 +200,13 @@ fn replace_parameter(input: &str, headers: &HeaderMap, data: &serde_json::Value)
|
|||
|
||||
fn get_string(value: &serde_json::Value) -> Result<String> {
|
||||
match &value {
|
||||
serde_json::Value::Null => unimplemented!(),
|
||||
serde_json::Value::Bool(bool) => Ok(bool.to_string()),
|
||||
serde_json::Value::Number(number) => Ok(number.to_string()),
|
||||
serde_json::Value::String(string) => Ok(string.as_str().to_string()),
|
||||
serde_json::Value::Array(_array) => unimplemented!(),
|
||||
serde_json::Value::Object(_object) => unimplemented!(),
|
||||
x => {
|
||||
error!("Could not get string from: {:?}", x);
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -228,15 +226,8 @@ fn filter_match(
|
|||
let parameter = parameter.trim();
|
||||
|
||||
if let Some(json_value) = data.pointer(parameter) {
|
||||
*data.pointer_mut(parameter).unwrap() = match json_value {
|
||||
serde_json::Value::Bool(bool) => serde_json::Value::String(bool.to_string()),
|
||||
serde_json::Value::String(string) => serde_json::Value::String(string.to_string()),
|
||||
serde_json::Value::Number(number) => serde_json::Value::String(number.to_string()),
|
||||
x => {
|
||||
error!("Could not get string from: {:?}", x);
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
*data.pointer_mut(parameter).unwrap() =
|
||||
serde_json::Value::String(get_string(json_value)?);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -246,7 +237,7 @@ fn filter_match(
|
|||
if regex.is_match(&value) {
|
||||
debug!("Filter `{}` of hook `{}` matched", filter_name, hook_name);
|
||||
|
||||
return Ok(Some(replace_parameter(
|
||||
return Ok(Some(replace_parameters(
|
||||
&hook.command,
|
||||
&request.headers(),
|
||||
data,
|
||||
|
@ -272,47 +263,56 @@ fn execute_hooks(request: &Request, data: Data) -> Result<Hooks, WebhookeyError>
|
|||
|
||||
let config = request.guard::<State<Config>>().unwrap(); // should never fail
|
||||
let mut valid = false;
|
||||
let mut hooks = HashMap::new();
|
||||
let mut result = HashMap::new();
|
||||
let client_ip = &request
|
||||
.client_ip()
|
||||
.unwrap_or(IpAddr::V4(Ipv4Addr::UNSPECIFIED));
|
||||
|
||||
for (hook_name, hook) in &config.hooks {
|
||||
if accept_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,);
|
||||
let hooks = config
|
||||
.hooks
|
||||
.iter()
|
||||
.filter(|(name, hook)| accept_ip(&name, &client_ip, &hook.ip_filter));
|
||||
|
||||
valid = true;
|
||||
for (hook_name, hook) in hooks {
|
||||
let signature = request
|
||||
.headers()
|
||||
.get_one(&hook.signature)
|
||||
.ok_or_else(|| WebhookeyError::InvalidSignature)?;
|
||||
|
||||
let mut data: serde_json::Value =
|
||||
serde_json::from_slice(&buffer).map_err(WebhookeyError::Serde)?;
|
||||
let secrets = hook
|
||||
.secrets
|
||||
.iter()
|
||||
.map(|secret| validate_request(&secret, &signature, &buffer));
|
||||
|
||||
for (filter_name, filter) in &hook.filters {
|
||||
match filter_match(
|
||||
&hook_name,
|
||||
&hook,
|
||||
&filter_name,
|
||||
&filter,
|
||||
&request,
|
||||
&mut data,
|
||||
) {
|
||||
Ok(Some(command)) => {
|
||||
hooks.insert(hook_name.to_string(), command);
|
||||
break;
|
||||
}
|
||||
Ok(None) => {}
|
||||
Err(e) => error!("{}", e),
|
||||
}
|
||||
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)?;
|
||||
|
||||
for (filter_name, filter) in &hook.filters {
|
||||
match filter_match(
|
||||
&hook_name,
|
||||
&hook,
|
||||
&filter_name,
|
||||
&filter,
|
||||
&request,
|
||||
&mut data,
|
||||
) {
|
||||
Ok(Some(command)) => {
|
||||
result.insert(hook_name.to_string(), command);
|
||||
break;
|
||||
}
|
||||
Ok(None) => {}
|
||||
Err(e) => error!("{}", e),
|
||||
}
|
||||
Err(e) => trace!("Hook `{}` could not validate request: {}", &hook_name, e),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Err(WebhookeyError::InvalidHeader);
|
||||
Err(e) => trace!("Hook `{}` could not validate request: {}", &hook_name, e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -321,7 +321,7 @@ fn execute_hooks(request: &Request, data: Data) -> Result<Hooks, WebhookeyError>
|
|||
return Err(WebhookeyError::Unauthorized(*client_ip));
|
||||
}
|
||||
|
||||
Ok(Hooks(hooks))
|
||||
Ok(Hooks { inner: result })
|
||||
}
|
||||
|
||||
impl FromDataSimple for Hooks {
|
||||
|
@ -330,7 +330,7 @@ impl FromDataSimple for Hooks {
|
|||
fn from_data(request: &Request, data: Data) -> data::Outcome<Self, Self::Error> {
|
||||
match execute_hooks(&request, data) {
|
||||
Ok(hooks) => {
|
||||
if hooks.0.is_empty() {
|
||||
if hooks.inner.is_empty() {
|
||||
let client_ip = &request
|
||||
.client_ip()
|
||||
.unwrap_or(IpAddr::V4(Ipv4Addr::UNSPECIFIED));
|
||||
|
@ -353,26 +353,21 @@ impl FromDataSimple for Hooks {
|
|||
}
|
||||
}
|
||||
|
||||
#[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 {
|
||||
info!("Execute `{}` from hook `{}`", &hook.1, &hook.0);
|
||||
for (name, command) in hooks.inner {
|
||||
info!("Execute `{}` from hook `{}`", &command, &name);
|
||||
|
||||
match run_script::run(&hook.1, &vec![], &ScriptOptions::new()) {
|
||||
match run_script::run(&command, &vec![], &ScriptOptions::new()) {
|
||||
Ok((status, stdout, stderr)) => {
|
||||
info!("Command `{}` exited with return code: {}", &hook.1, status);
|
||||
trace!("Output of command `{}` on stdout: {:?}", &hook.1, &stdout);
|
||||
debug!("Output of command `{}` on stderr: {:?}", &hook.1, &stderr);
|
||||
info!("Command `{}` exited with return code: {}", &command, status);
|
||||
trace!("Output of command `{}` on stdout: {:?}", &command, &stdout);
|
||||
debug!("Output of command `{}` on stderr: {:?}", &command, &stderr);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Execution of `{}` failed: {}", &hook.1, e);
|
||||
error!("Execution of `{}` failed: {}", &command, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -381,12 +376,14 @@ fn receive_hook<'a>(address: SocketAddr, hooks: Hooks) -> Result<Response<'a>> {
|
|||
}
|
||||
|
||||
fn get_config() -> Result<File> {
|
||||
// Look for systemwide config..
|
||||
if let Ok(config) = File::open("/etc/webhookey/config.yml") {
|
||||
info!("Loading configuration from `/etc/webhookey/config.yml`");
|
||||
|
||||
return Ok(config);
|
||||
}
|
||||
|
||||
// ..look for user path config..
|
||||
if let Some(mut path) = dirs::config_dir() {
|
||||
path.push("webhookey/config.yml");
|
||||
|
||||
|
@ -400,12 +397,14 @@ fn get_config() -> Result<File> {
|
|||
}
|
||||
}
|
||||
|
||||
// ..look for config in CWD..
|
||||
if let Ok(config) = File::open("config.yml") {
|
||||
info!("Loading configuration from `./config.yml`");
|
||||
|
||||
return Ok(config);
|
||||
}
|
||||
|
||||
// ..you had your chance.
|
||||
bail!("No configuration file found.");
|
||||
}
|
||||
|
||||
|
@ -417,7 +416,7 @@ fn main() -> Result<()> {
|
|||
trace!("Parsed configuration:\n{}", serde_yaml::to_string(&config)?);
|
||||
|
||||
rocket::ignite()
|
||||
.mount("/", routes![index, receive_hook])
|
||||
.mount("/", routes![receive_hook])
|
||||
.attach(AdHoc::on_attach("webhookey config", move |rocket| {
|
||||
Ok(rocket.manage(config))
|
||||
}))
|
||||
|
@ -435,17 +434,6 @@ mod tests {
|
|||
};
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn index() {
|
||||
let rocket = rocket::ignite().mount("/", routes![index]);
|
||||
|
||||
let client = Client::new(rocket).unwrap();
|
||||
let mut response = client.get("/").dispatch();
|
||||
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
assert_eq!(response.body_string(), Some("Hello, webhookey!".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn secret() {
|
||||
let mut hooks = HashMap::new();
|
||||
|
@ -520,42 +508,42 @@ mod tests {
|
|||
map.add_raw("X-Gitea-Event", "something");
|
||||
|
||||
assert_eq!(
|
||||
replace_parameter("command", &map, &serde_json::Value::Null).unwrap(),
|
||||
replace_parameters("command", &map, &serde_json::Value::Null).unwrap(),
|
||||
"command"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
replace_parameter(" command", &map, &serde_json::Value::Null).unwrap(),
|
||||
replace_parameters(" command", &map, &serde_json::Value::Null).unwrap(),
|
||||
" command"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
replace_parameter("command ", &map, &serde_json::Value::Null).unwrap(),
|
||||
replace_parameters("command ", &map, &serde_json::Value::Null).unwrap(),
|
||||
"command "
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
replace_parameter(" command ", &map, &serde_json::Value::Null).unwrap(),
|
||||
replace_parameters(" command ", &map, &serde_json::Value::Null).unwrap(),
|
||||
" command "
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
replace_parameter("command command ", &map, &serde_json::Value::Null).unwrap(),
|
||||
replace_parameters("command command ", &map, &serde_json::Value::Null).unwrap(),
|
||||
"command command "
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
replace_parameter("{{ /foo }} command", &map, &json!({ "foo": "bar" })).unwrap(),
|
||||
replace_parameters("{{ /foo }} command", &map, &json!({ "foo": "bar" })).unwrap(),
|
||||
"bar command"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
replace_parameter(" command {{ /foo }} ", &map, &json!({ "foo": "bar" })).unwrap(),
|
||||
replace_parameters(" command {{ /foo }} ", &map, &json!({ "foo": "bar" })).unwrap(),
|
||||
" command bar "
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
replace_parameter(
|
||||
replace_parameters(
|
||||
"{{ /foo }} command{{/field1/foo}}",
|
||||
&map,
|
||||
&json!({ "foo": "bar", "field1": { "foo": "baz" } })
|
||||
|
@ -565,12 +553,12 @@ mod tests {
|
|||
);
|
||||
|
||||
assert_eq!(
|
||||
replace_parameter(" command {{ /foo }} ", &map, &json!({ "foo": "bar" })).unwrap(),
|
||||
replace_parameters(" command {{ /foo }} ", &map, &json!({ "foo": "bar" })).unwrap(),
|
||||
" command bar "
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
replace_parameter(
|
||||
replace_parameters(
|
||||
" {{ /field1/foo }} command",
|
||||
&map,
|
||||
&json!({ "field1": { "foo": "bar" } })
|
||||
|
@ -580,7 +568,7 @@ mod tests {
|
|||
);
|
||||
|
||||
assert_eq!(
|
||||
replace_parameter(
|
||||
replace_parameters(
|
||||
" {{ header X-Gitea-Event }} command",
|
||||
&map,
|
||||
&json!({ "field1": { "foo": "bar" } })
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue