add gitea sig header check, add Json errors, begin AppState

This commit is contained in:
2026-05-31 20:32:49 +00:00
parent c119bed142
commit aa746f357d
10 changed files with 288 additions and 37 deletions
+63 -23
View File
@@ -1,16 +1,28 @@
use axum::response::{Response, IntoResponse};
use axum::{Json, Router};
use anyhow::anyhow;
use axum::body::to_bytes;
use axum::extract::FromRequest;
use axum::routing::{post, get};
use reqwest::StatusCode;
use axum::response::{IntoResponse, Response};
use axum::routing::{get, post};
use axum::{Json, Router};
use hmac::{Hmac, KeyInit, Mac};
use serde_json::Value;
use sha2::Sha256;
use subtle::ConstantTimeEq;
use crate::env::{EnvConfig};
use crate::errors::AppError;
use crate::gitea::WebhookType;
use crate::state::AppState;
pub async fn start_api(config: EnvConfig) -> anyhow::Result<()> {
let app = Router::new().route("/", get(root)).route("/webhook", post(webhook));
let listerner = tokio::net::TcpListener::bind(format!("0.0.0.0:{}", config.http_port)).await?;
const MAX_WEBHOOK_BODY_SIZE: usize = 1024 * 1024; // 1 Mo
pub async fn start(app_state: AppState) -> anyhow::Result<()> {
let http_port = app_state.config.http_port;
let app = Router::new()
.with_state(app_state)
.route("/", get(root))
.route("/webhook", post(webhook));
let listerner = tokio::net::TcpListener::bind(format!("0.0.0.0:{}", http_port)).await?;
axum::serve(listerner, app)
.await
@@ -21,11 +33,12 @@ async fn root() -> &'static str {
"Hi, i'm Herald :)"
}
async fn webhook(WebhookExtract(wb): WebhookExtract) -> Response {
match wb {
async fn webhook(WebhookExtract(wb): WebhookExtract) -> Result<Response, AppError> {
Ok(match wb {
WebhookType::Review(id, _) => format!("Received {} pr id", id),
_ => String::from("Nothing to see :/")
}.into_response()
_ => String::from("Nothing to see :/"),
}
.into_response())
}
pub struct WebhookExtract(pub WebhookType);
@@ -34,21 +47,48 @@ impl<S> FromRequest<S> for WebhookExtract
where
S: Send + Sync,
{
type Rejection = Response;
type Rejection = AppError;
async fn from_request(
req: axum::extract::Request,
state: &S,
) -> Result<Self, Self::Rejection> {
let Json(value) = Json::<Value>::from_request(req, state)
async fn from_request(req: axum::extract::Request, state: &S) -> Result<Self, Self::Rejection> {
let headers = req.headers();
let sig_header = headers
.get("x-gitea-signature")
.ok_or(AppError::WebHookSigHeaderNotFoundErr)?
.to_str()
.map_err(|err| anyhow!(err))?
.to_string();
let body = req.into_body();
let body_bytes = to_bytes(body, MAX_WEBHOOK_BODY_SIZE)
.await
.map_err(|e| e.into_response())?;
.map_err(|err| anyhow!(err))?;
let webhook = WebhookType::try_from(value)
.map_err(|e| {
(StatusCode::UNPROCESSABLE_ENTITY, e.to_string()).into_response()
})?;
let Json(value) =
Json::<Value>::from_bytes(&body_bytes).map_err(|_| AppError::MalformedJsonErr)?;
let webhook = WebhookType::try_from(value)?;
check_sig_header(sig_header.as_bytes(), &body_bytes)?;
Ok(WebhookExtract(webhook))
}
}
fn check_sig_header(sig_header: &[u8], body: &[u8]) -> Result<(), AppError> {
let sig_header_decoded = hex::decode(sig_header).map_err(|err| anyhow!(err))?;
let webhook_sig_header_secret =
std::env::var("WEBHOOK_SIG_HEADER_SECRET").map_err(|err| anyhow!(err))?;
let mut mac = Hmac::<Sha256>::new_from_slice(&webhook_sig_header_secret.into_bytes())
.map_err(|err| anyhow!(err))?;
mac.update(body);
let generated_hmac = mac.finalize().into_bytes();
let check_result: bool = generated_hmac.ct_eq(&sig_header_decoded).into();
match check_result {
true => Ok(()),
false => Err(AppError::WebHookSigHeaderInvalidErr),
}
}