use axum::body::{Bytes, to_bytes}; use axum::extract::{FromRef, FromRequest, State}; use axum::response::IntoResponse; use axum::routing::{get, post}; use axum::{Json, Router}; use hmac::{Hmac, KeyInit, Mac}; use reqwest::StatusCode; use serde_json::Value; use sha2::Sha256; use subtle::ConstantTimeEq; use crate::consts::{GITEA_EVENT_TYPE_HEADER_NAME, GITEA_SIG_HEADER_NAME, MAX_WEBHOOK_BODY_SIZE}; use crate::errors::AppError; use crate::gitea::WebhookType; use crate::state::AppState; pub async fn start(app_state: AppState) -> anyhow::Result<()> { let http_port = app_state.config.http_port; let app = Router::new() .route("/", get(root)) .route("/webhook", post(webhook)) .with_state(app_state); let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{}", http_port)).await?; axum::serve(listener, app) .await .map_err(anyhow::Error::from) } async fn root() -> &'static str { "Hi, i'm Herald :)" } async fn webhook( State(app_state): State, WebhookExtract(wb): WebhookExtract, ) -> Result { app_state .bot_tx .send(wb) .await .map_err(anyhow::Error::from)?; Ok((StatusCode::CREATED, "Task started")) } pub struct WebhookExtract(pub WebhookType); impl FromRequest for WebhookExtract where AppState: FromRef, S: Send + Sync, { type Rejection = AppError; async fn from_request(req: axum::extract::Request, state: &S) -> Result { let app_state = AppState::from_ref(state); let headers = req.headers(); let sig_header = extract_header(GITEA_SIG_HEADER_NAME, headers)?; let type_header = extract_header(GITEA_EVENT_TYPE_HEADER_NAME, headers)?; let body_bytes = read_body(req.into_body()).await?; verify_signature( app_state.config.webhook_secret.as_bytes(), &sig_header, &body_bytes, )?; let webhook = parse_webhook(&type_header, &app_state.config.bot_name, &body_bytes)?; Ok(WebhookExtract(webhook)) } } fn extract_header(key: &str, headers: &axum::http::HeaderMap) -> Result { let value = headers .get(key) .ok_or(AppError::WebHookMissingHeaderErr(key.into()))? .to_str() .map_err(anyhow::Error::from)?; Ok(value.to_owned()) } async fn read_body(body: axum::body::Body) -> Result { to_bytes(body, MAX_WEBHOOK_BODY_SIZE) .await .map_err(anyhow::Error::from) .map_err(AppError::from) } fn parse_webhook(header: &str, bot_name: &str, body_bytes: &[u8]) -> Result { let Json(value) = Json::::from_bytes(body_bytes).map_err(|_| AppError::MalformedJsonErr)?; WebhookType::from_event(header, bot_name, value) } fn verify_signature(secret_key: &[u8], sig_header: &str, body: &[u8]) -> Result<(), AppError> { let sig_header_decoded = hex::decode(sig_header).map_err(|_| AppError::WebHookSigHeaderInvalidErr)?; let mut mac = Hmac::::new_from_slice(secret_key).map_err(anyhow::Error::from)?; mac.update(body); let generated_hmac = mac.finalize().into_bytes(); bool::from(generated_hmac.ct_eq(&sig_header_decoded)) .then_some(()) .ok_or(AppError::WebHookSigHeaderInvalidErr) }