112 lines
3.3 KiB
Rust
112 lines
3.3 KiB
Rust
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<AppState>,
|
|
WebhookExtract(wb): WebhookExtract,
|
|
) -> Result<impl IntoResponse, AppError> {
|
|
app_state
|
|
.bot_tx
|
|
.send(wb)
|
|
.await
|
|
.map_err(anyhow::Error::from)?;
|
|
|
|
Ok((StatusCode::CREATED, "Task started"))
|
|
}
|
|
|
|
pub struct WebhookExtract(pub WebhookType);
|
|
|
|
impl<S> FromRequest<S> for WebhookExtract
|
|
where
|
|
AppState: FromRef<S>,
|
|
S: Send + Sync,
|
|
{
|
|
type Rejection = AppError;
|
|
|
|
async fn from_request(req: axum::extract::Request, state: &S) -> Result<Self, Self::Rejection> {
|
|
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<String, AppError> {
|
|
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<Bytes, AppError> {
|
|
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<WebhookType, AppError> {
|
|
let Json(value) =
|
|
Json::<Value>::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::<Sha256>::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)
|
|
}
|