Hostityourself/server/src/routes/webhooks.rs
Claude 8f5bb158cb
M1: Rust control plane, builder, dashboard, and infra
- Cargo workspace with hiy-server (axum 0.7 + sqlx SQLite + tokio)
- SQLite schema: apps, deploys, env_vars (inline migrations, no daemon)
- Background build worker: sequential queue, streams stdout/stderr to DB
- REST API: CRUD for apps, deploys, env vars; GitHub webhook with HMAC-SHA256
- SSE endpoint for live build log streaming
- Monospace HTMX-free dashboard: app list + per-app detail, log viewer, env editor
- builder/build.sh: clone/pull → detect strategy (Dockerfile/buildpack/static)
  → docker build → swap container → update Caddy via admin API → prune images
- infra/docker-compose.yml + Dockerfile.server for local dev (no Pi needed)
- proxy/Caddyfile: auto-HTTPS off for local, comment removed for production
- .env.example

Compiles clean (zero warnings). Run locally:
  cp .env.example .env && cargo run --bin hiy-server

https://claude.ai/code/session_01FKCW3FDjNFj6jve4niMFXH
2026-03-19 08:25:59 +00:00

77 lines
2.2 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("");
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 {
return StatusCode::OK; // different branch — silently ignore
}
let sha = payload["after"].as_str().map(String::from);
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
}