github build listener

This commit is contained in:
mbecker20
2023-07-03 06:48:58 +00:00
parent 3da824b4c3
commit b08d5090a2
6 changed files with 139 additions and 1 deletions

1
Cargo.lock generated
View File

@@ -1826,6 +1826,7 @@ dependencies = [
"dotenv",
"envy",
"futures",
"hex",
"hmac",
"jwt",
"log",

View File

@@ -47,6 +47,7 @@ jwt = "0.16"
hmac = "0.12"
sha2 = "0.10"
bcrypt = "0.14"
hex = "0.4"
aws-config = "0.55"
aws-sdk-ec2 = "0.28"
proc-macro2 = "1.0"

View File

@@ -36,6 +36,7 @@ jwt.workspace = true
hmac.workspace = true
sha2.workspace = true
bcrypt.workspace = true
hex.workspace = true
async-trait.workspace = true
futures.workspace = true
aws-config.workspace = true

128
core/src/github_listener.rs Normal file
View File

@@ -0,0 +1,128 @@
use anyhow::{anyhow, Context};
use axum::{extract::Path, http::HeaderMap, routing::post, Router};
use hex::ToHex;
use hmac::{Hmac, Mac};
use monitor_types::requests::api;
use resolver_api::Resolve;
use serde::Deserialize;
use sha2::Sha256;
use crate::{
auth::InnerRequestUser,
helpers::random_duration,
state::{State, StateExtension},
};
type HmacSha256 = Hmac<Sha256>;
#[derive(Deserialize)]
struct Id {
id: String,
}
pub fn router() -> Router {
Router::new().route(
"/build/:id",
post(
|state: StateExtension, Path(Id { id }), headers: HeaderMap, body: String| async move {
tokio::spawn(async move {
let res = state.handle_build_webhook(id.clone(), headers, body).await;
if let Err(e) = res {
warn!("failed to run build webook for build {id} | {e:#?}");
}
});
},
),
)
}
impl State {
async fn handle_build_webhook(
&self,
build_id: String,
headers: HeaderMap,
body: String,
) -> anyhow::Result<()> {
self.verify_gh_signature(headers, &body).await?;
let request_branch = extract_branch(&body)?;
let expected_branch = self.get_build(&build_id).await?.config.branch;
if request_branch != expected_branch {
return Err(anyhow!("request branch does not match expected"));
}
self.resolve(
api::RunBuild { build_id },
InnerRequestUser {
id: String::from("github"),
is_admin: true,
create_server_permissions: false,
create_build_permissions: false,
}
.into(),
)
.await?;
Ok(())
}
// async fn handle_procedure_webhook(
// &self,
// id: &str,
// headers: HeaderMap,
// body: String,
// ) -> anyhow::Result<()> {
// self.verify_gh_signature(headers, &body).await?;
// let request_branch = extract_branch(&body)?;
// let expected_branches = self.db.get_procedure(id).await?.webhook_branches;
// if !expected_branches.contains(&request_branch) {
// return Err(anyhow!("request branch does not match expected"));
// }
// self.run_procedure(
// id,
// &RequestUser {
// id: String::from(GITHUB_WEBHOOK_USER_ID),
// is_admin: true,
// create_server_permissions: false,
// create_build_permissions: false,
// },
// )
// .await?;
// Ok(())
// }
async fn verify_gh_signature(&self, headers: HeaderMap, body: &str) -> anyhow::Result<()> {
// wait random amount of time
tokio::time::sleep(random_duration(0, 500)).await;
let signature = headers.get("x-hub-signature-256");
if signature.is_none() {
return Err(anyhow!("no signature in headers"));
}
let signature = signature.unwrap().to_str();
if signature.is_err() {
return Err(anyhow!("failed to unwrap signature"));
}
let signature = signature.unwrap().replace("sha256=", "");
let mut mac = HmacSha256::new_from_slice(self.config.github_webhook_secret.as_bytes())
.expect("github webhook | failed to create hmac sha256");
mac.update(body.as_bytes());
let expected = mac.finalize().into_bytes().encode_hex::<String>();
if signature == expected {
Ok(())
} else {
Err(anyhow!("signature does not equal expected"))
}
}
}
#[derive(Deserialize)]
struct GithubWebhookBody {
#[serde(rename = "ref")]
branch: String,
}
fn extract_branch(body: &str) -> anyhow::Result<String> {
let branch = serde_json::from_str::<GithubWebhookBody>(body)
.context("failed to parse github request body")?
.branch
.replace("refs/heads/", "");
Ok(branch)
}

View File

@@ -1,4 +1,4 @@
use std::collections::HashMap;
use std::{collections::HashMap, time::Duration};
use anyhow::{anyhow, Context};
use monitor_types::{
@@ -15,6 +15,7 @@ use monitor_types::{
permissioned::Permissioned,
};
use periphery_client::{requests, PeripheryClient};
use rand::{thread_rng, Rng};
use tokio::sync::RwLock;
use crate::{auth::RequestUser, state::State};
@@ -363,3 +364,7 @@ pub fn empty_or_only_spaces(word: &str) -> bool {
}
true
}
pub fn random_duration(min_ms: u64, max_ms: u64) -> Duration {
Duration::from_millis(thread_rng().gen_range(min_ms..max_ms))
}

View File

@@ -8,6 +8,7 @@ mod auth;
mod cloud;
mod config;
mod db;
mod github_listener;
mod helpers;
mod monitoring;
mod requests;
@@ -24,6 +25,7 @@ async fn app() -> anyhow::Result<()> {
let app = Router::new()
.nest("/auth", auth::router(&state))
.nest("/api", requests::api::router())
.nest("/listener", github_listener::router())
.nest("/ws", ws::router())
.layer(Extension(state));