Hostityourself/server/src/routes/webhooks.rs
Claude 8dab4231ea
Add info logging to webhook handler
Makes it easy to see if GitHub is hitting the endpoint, whether the
signature check passes, and whether a deploy is triggered.

https://claude.ai/code/session_01FKCW3FDjNFj6jve4niMFXH
2026-03-20 09:36:25 +00:00

81 lines
2.4 KiB
Rust

use axum::{
body::Bytes,
extract::{Path, State},
http::{HeaderMap, StatusCode},
};
use hmac::{Hmac, Mac};
use sha2::Sha256;
use crate::{builder, AppState};
type HmacSha256 = Hmac<Sha256>;
pub async fn github(
State(s): State<AppState>,
Path(app_id): Path<String>,
headers: HeaderMap,
body: Bytes,
) -> StatusCode {
let app = match sqlx::query_as::<_, crate::models::App>("SELECT * FROM apps WHERE id = ?")
.bind(&app_id)
.fetch_optional(&s.db)
.await
{
Ok(Some(a)) => a,
_ => return StatusCode::NOT_FOUND,
};
// Verify HMAC-SHA256 signature.
let sig = headers
.get("x-hub-signature-256")
.and_then(|v| v.to_str().ok())
.unwrap_or("");
tracing::info!("Webhook received for app {}", app_id);
if !verify_sig(&app.webhook_secret, &body, sig) {
tracing::warn!("Bad webhook signature for app {}", app_id);
return StatusCode::UNAUTHORIZED;
}
// Only deploy on pushes to the configured branch.
let payload: serde_json::Value = match serde_json::from_slice(&body) {
Ok(v) => v,
Err(_) => return StatusCode::BAD_REQUEST,
};
let pushed_ref = payload["ref"].as_str().unwrap_or("");
let expected_ref = format!("refs/heads/{}", app.branch);
if pushed_ref != expected_ref {
tracing::info!("Webhook for app {}: ignoring push to {} (watching {})", app_id, pushed_ref, expected_ref);
return StatusCode::OK; // different branch — silently ignore
}
let sha = payload["after"].as_str().map(String::from);
tracing::info!("Webhook triggering deploy for app {} sha={:?}", app_id, sha);
if let Err(e) = builder::enqueue_deploy(&s, &app_id, "webhook", sha).await {
tracing::error!("Enqueue deploy for {}: {}", app_id, e);
return StatusCode::INTERNAL_SERVER_ERROR;
}
StatusCode::OK
}
/// Constant-time HMAC-SHA256 signature check.
fn verify_sig(secret: &str, body: &[u8], sig_header: &str) -> bool {
let mut mac = match HmacSha256::new_from_slice(secret.as_bytes()) {
Ok(m) => m,
Err(_) => return false,
};
mac.update(body);
let expected = format!("sha256={}", hex::encode(mac.finalize().into_bytes()));
// Constant-time compare to avoid timing side-channels.
expected.len() == sig_header.len()
&& expected
.bytes()
.zip(sig_header.bytes())
.fold(0u8, |acc, (a, b)| acc | (a ^ b))
== 0
}