Restructuring of from_data()
and configuration
For better readability, correctness and maintainability the body (which is still rather large, though) was restructured. Regarding the signature, to be able to configure different fields in the HTTP header the configuration parameter signature was added.
This commit is contained in:
parent
84c24e9129
commit
2c00441b34
2 changed files with 78 additions and 97 deletions
|
@ -61,6 +61,7 @@ each hook has to be configured, It contains following fields:
|
||||||
with `{{ /field/pointed/to }}`. Further `{{ event }}` and `{{
|
with `{{ /field/pointed/to }}`. Further `{{ event }}` and `{{
|
||||||
signature }}` are valid variables as they contain the values from
|
signature }}` are valid variables as they contain the values from
|
||||||
the regarding header fields of the http request.
|
the regarding header fields of the http request.
|
||||||
|
- signature: Name of the HTTP header field containing the signature.
|
||||||
- secrets: List of secrets.
|
- secrets: List of secrets.
|
||||||
- filters: List of filters.
|
- filters: List of filters.
|
||||||
|
|
||||||
|
|
174
src/main.rs
174
src/main.rs
|
@ -41,6 +41,7 @@ struct Config {
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
struct Hook {
|
struct Hook {
|
||||||
command: String,
|
command: String,
|
||||||
|
signature: String,
|
||||||
secrets: Vec<String>,
|
secrets: Vec<String>,
|
||||||
filters: HashMap<String, Filter>,
|
filters: HashMap<String, Filter>,
|
||||||
}
|
}
|
||||||
|
@ -52,7 +53,7 @@ struct Filter {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct Hooks(HashMap<String, Vec<String>>);
|
struct Hooks(HashMap<String, String>);
|
||||||
|
|
||||||
fn validate_request(secret: &str, signature: &str, data: &[u8]) -> Result<()> {
|
fn validate_request(secret: &str, signature: &str, data: &[u8]) -> Result<()> {
|
||||||
let mut mac = Hmac::<Sha256>::new_varkey(&secret.as_bytes())
|
let mut mac = Hmac::<Sha256>::new_varkey(&secret.as_bytes())
|
||||||
|
@ -118,7 +119,7 @@ fn filter_match(
|
||||||
)?));
|
)?));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
anyhow!(
|
bail!(
|
||||||
"Could not parse pointer in hook `{}` from filter `{}`",
|
"Could not parse pointer in hook `{}` from filter `{}`",
|
||||||
hook_name,
|
hook_name,
|
||||||
filter_name
|
filter_name
|
||||||
|
@ -139,40 +140,28 @@ impl FromDataSimple for Hooks {
|
||||||
type Error = anyhow::Error;
|
type Error = anyhow::Error;
|
||||||
|
|
||||||
fn from_data(request: &Request, data: Data) -> data::Outcome<Self, Self::Error> {
|
fn from_data(request: &Request, data: Data) -> data::Outcome<Self, Self::Error> {
|
||||||
let config = request.guard::<State<Config>>().unwrap(); // should never fail
|
let mut buffer = Vec::new();
|
||||||
|
match data.open().read_to_end(&mut buffer) {
|
||||||
|
Ok(size) => info!("Data of size {} received", size),
|
||||||
|
Err(e) => {
|
||||||
|
error!("Could not read to end of data: {}", &e);
|
||||||
|
return Failure((
|
||||||
|
Status::BadRequest,
|
||||||
|
anyhow!("Could not read to end of data: {}", &e),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let config = request.guard::<State<Config>>().unwrap(); // should never fail
|
||||||
|
let mut valid = false;
|
||||||
let mut hooks = HashMap::new();
|
let mut hooks = HashMap::new();
|
||||||
|
|
||||||
if let Some(signature) = request.headers().get_one("X-Gitea-Signature") {
|
for (hook_name, hook) in &config.hooks {
|
||||||
let mut data = data.open();
|
if let Some(signature) = request.headers().get_one(&hook.signature) {
|
||||||
let mut buffer = Vec::new();
|
|
||||||
|
|
||||||
match data.read_to_end(&mut buffer) {
|
|
||||||
Ok(_) => {}
|
|
||||||
Err(e) => {
|
|
||||||
error!("Could not read to end of data: {}", &e);
|
|
||||||
return Failure((
|
|
||||||
Status::BadRequest,
|
|
||||||
anyhow!("Could not read to end of data: {}", &e),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
trace!("Data received: {:?}", from_utf8(&buffer));
|
|
||||||
|
|
||||||
let mut valid = false;
|
|
||||||
|
|
||||||
for (hook_name, hook) in &config.hooks {
|
|
||||||
let mut commands = Vec::new();
|
|
||||||
|
|
||||||
for secret in &hook.secrets {
|
for secret in &hook.secrets {
|
||||||
match validate_request(&secret, &signature, &buffer) {
|
match validate_request(&secret, &signature, &buffer) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
trace!(
|
trace!("Valid signature found for hook `{}`", hook_name,);
|
||||||
"Valid signature found for hook `{}`: {}",
|
|
||||||
hook_name,
|
|
||||||
signature
|
|
||||||
);
|
|
||||||
|
|
||||||
valid = true;
|
valid = true;
|
||||||
|
|
||||||
|
@ -196,7 +185,10 @@ impl FromDataSimple for Hooks {
|
||||||
&request,
|
&request,
|
||||||
&data,
|
&data,
|
||||||
) {
|
) {
|
||||||
Ok(Some(command)) => commands.push(command),
|
Ok(Some(command)) => {
|
||||||
|
hooks.insert(hook_name.to_string(), command);
|
||||||
|
break;
|
||||||
|
}
|
||||||
Ok(None) => {}
|
Ok(None) => {}
|
||||||
Err(e) => error!("{}", e),
|
Err(e) => error!("{}", e),
|
||||||
}
|
}
|
||||||
|
@ -211,51 +203,32 @@ impl FromDataSimple for Hooks {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !commands.is_empty() {
|
|
||||||
hooks.insert(hook_name.to_string(), commands);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if hooks.is_empty() {
|
|
||||||
if valid {
|
|
||||||
warn!(
|
|
||||||
"Unmatched hook from {:?} with signature {:?}",
|
|
||||||
&request.client_ip(),
|
|
||||||
&request.headers().get_one("X-Gitea-Signature")
|
|
||||||
);
|
|
||||||
Failure((
|
|
||||||
Status::NotFound,
|
|
||||||
anyhow!(
|
|
||||||
"Unmatched hook from {:?} with signature {:?}",
|
|
||||||
&request.client_ip(),
|
|
||||||
&request.headers().get_one("X-Gitea-Signature")
|
|
||||||
),
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
warn!(
|
|
||||||
"Unauthorized request from {:?} with signature {:?}",
|
|
||||||
&request.client_ip(),
|
|
||||||
&request.headers().get_one("X-Gitea-Signature")
|
|
||||||
);
|
|
||||||
Failure((
|
|
||||||
Status::Unauthorized,
|
|
||||||
anyhow!(
|
|
||||||
"Unauthorized request from {:?} with signature {:?}",
|
|
||||||
&request.client_ip(),
|
|
||||||
&request.headers().get_one("X-Gitea-Signature")
|
|
||||||
),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
Success(Hooks(hooks))
|
error!("Could not extract signature from header");
|
||||||
|
return Failure((
|
||||||
|
Status::BadRequest,
|
||||||
|
anyhow!("Could not extract signature from header"),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
Failure((
|
|
||||||
Status::BadRequest,
|
|
||||||
anyhow!("Could not extract signature from header"),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if hooks.is_empty() {
|
||||||
|
if valid {
|
||||||
|
warn!("Unmatched hook from {:?}", &request.client_ip());
|
||||||
|
return Failure((
|
||||||
|
Status::NotFound,
|
||||||
|
anyhow!("Unmatched hook from {:?}", &request.client_ip()),
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
error!("Unauthorized request from {:?}", &request.client_ip());
|
||||||
|
return Failure((
|
||||||
|
Status::Unauthorized,
|
||||||
|
anyhow!("Unauthorized request from {:?}", &request.client_ip()),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Success(Hooks(hooks))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -269,30 +242,28 @@ fn receive_hook<'a>(address: SocketAddr, hooks: Hooks) -> Result<Response<'a>> {
|
||||||
info!("Post request received from: {}", address);
|
info!("Post request received from: {}", address);
|
||||||
|
|
||||||
for hook in hooks.0 {
|
for hook in hooks.0 {
|
||||||
for command in hook.1 {
|
info!("Execute `{}` from hook `{}`", &hook.1, &hook.0);
|
||||||
info!("Execute `{}` from hook `{}`", &command, &hook.0);
|
|
||||||
|
|
||||||
let command = command.split(' ').collect::<Vec<&str>>();
|
let command = hook.1.split(' ').collect::<Vec<&str>>();
|
||||||
match Command::new(&command[0]).args(&command[1..]).output() {
|
match Command::new(&command[0]).args(&command[1..]).output() {
|
||||||
Ok(executed) => {
|
Ok(executed) => {
|
||||||
info!(
|
info!(
|
||||||
"Command `{}` exited with return code: {}",
|
"Command `{}` exited with return code: {}",
|
||||||
&command[0], &executed.status
|
&command[0], &executed.status
|
||||||
);
|
);
|
||||||
trace!(
|
trace!(
|
||||||
"Output of command `{}` on stdout: {:?}",
|
"Output of command `{}` on stdout: {:?}",
|
||||||
&command[0],
|
&command[0],
|
||||||
from_utf8(&executed.stdout)?
|
from_utf8(&executed.stdout)?
|
||||||
);
|
);
|
||||||
debug!(
|
debug!(
|
||||||
"Output of command `{}` on stderr: {:?}",
|
"Output of command `{}` on stderr: {:?}",
|
||||||
&command[0],
|
&command[0],
|
||||||
from_utf8(&executed.stderr)?
|
from_utf8(&executed.stderr)?
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Execution of `{}` failed: {}", command[0], e);
|
error!("Execution of `{}` failed: {}", command[0], e);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -373,6 +344,7 @@ mod tests {
|
||||||
"test_hook".to_string(),
|
"test_hook".to_string(),
|
||||||
Hook {
|
Hook {
|
||||||
command: "".to_string(),
|
command: "".to_string(),
|
||||||
|
signature: "X-Gitea-Signature".to_string(),
|
||||||
secrets: vec!["valid".to_string()],
|
secrets: vec!["valid".to_string()],
|
||||||
filters: HashMap::new(),
|
filters: HashMap::new(),
|
||||||
},
|
},
|
||||||
|
@ -497,6 +469,14 @@ mod tests {
|
||||||
" bar command"
|
" bar command"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add tests with header fields
|
assert_eq!(
|
||||||
|
replace_parameter(
|
||||||
|
" {{ event }} command",
|
||||||
|
&map,
|
||||||
|
&json!({ "field1": { "foo": "bar" } })
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
" something command"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue