2024-07-16 01:02:30 +00:00
|
|
|
use std::future::Future;
|
|
|
|
|
2024-07-14 22:33:24 +00:00
|
|
|
use axum::async_trait;
|
2024-07-16 01:02:30 +00:00
|
|
|
use axum::body::Body;
|
|
|
|
use axum::body::Bytes;
|
2024-07-14 22:33:24 +00:00
|
|
|
use axum::extract::FromRequest;
|
|
|
|
use axum::extract::Request;
|
2024-07-26 00:04:30 +00:00
|
|
|
use axum::extract::State;
|
2024-07-14 20:38:07 +00:00
|
|
|
use axum::http::HeaderMap;
|
2024-07-14 20:13:06 +00:00
|
|
|
use axum::http::StatusCode;
|
2024-07-16 01:02:30 +00:00
|
|
|
use axum::middleware::Next;
|
2024-07-14 22:33:24 +00:00
|
|
|
use axum::response::IntoResponse;
|
|
|
|
use axum::response::Response;
|
2024-07-14 20:13:06 +00:00
|
|
|
use axum::Json;
|
2024-07-14 22:33:24 +00:00
|
|
|
use axum::RequestExt;
|
2024-09-29 19:03:07 +00:00
|
|
|
use base64::engine::general_purpose;
|
|
|
|
use base64::Engine as _;
|
2024-07-16 01:02:30 +00:00
|
|
|
use hmac::Hmac;
|
|
|
|
use hmac::Mac;
|
|
|
|
use http_body_util::BodyExt;
|
2024-07-14 20:13:06 +00:00
|
|
|
use serde::Serialize;
|
2024-07-16 01:02:30 +00:00
|
|
|
use sha2::Sha256;
|
2024-07-14 22:33:24 +00:00
|
|
|
use tracing::debug;
|
|
|
|
|
2024-09-29 18:32:20 +00:00
|
|
|
use crate::app_state::AppState;
|
2024-09-29 04:48:39 +00:00
|
|
|
use crate::discovery::discover_matching_push_triggers;
|
|
|
|
use crate::discovery::discover_webhook_bridge_config;
|
|
|
|
use crate::gitea_client::GiteaClient;
|
2024-07-14 22:33:24 +00:00
|
|
|
use crate::hook_push::HookPush;
|
2024-09-29 04:48:39 +00:00
|
|
|
use crate::hook_push::PipelineParamters;
|
|
|
|
use crate::kubernetes::run_pipelines;
|
2024-07-14 20:13:06 +00:00
|
|
|
|
2024-07-16 01:02:30 +00:00
|
|
|
type HmacSha256 = Hmac<Sha256>;
|
|
|
|
|
2024-07-14 20:38:07 +00:00
|
|
|
pub(crate) async fn hook(
|
2024-07-14 22:33:24 +00:00
|
|
|
_headers: HeaderMap,
|
2024-07-26 00:04:30 +00:00
|
|
|
State(state): State<AppState>,
|
2024-07-14 22:33:24 +00:00
|
|
|
payload: HookRequest,
|
2024-07-14 20:38:07 +00:00
|
|
|
) -> (StatusCode, Json<HookResponse>) {
|
2024-07-14 22:33:24 +00:00
|
|
|
debug!("REQ: {:?}", payload);
|
|
|
|
match payload {
|
2024-09-29 04:48:39 +00:00
|
|
|
HookRequest::Push(webhook_payload) => {
|
|
|
|
handle_push(state.gitea, state.kubernetes_client, webhook_payload)
|
|
|
|
.await
|
|
|
|
.expect("Failed to handle push event.");
|
|
|
|
(
|
|
|
|
StatusCode::OK,
|
|
|
|
Json(HookResponse {
|
|
|
|
ok: true,
|
|
|
|
message: None,
|
|
|
|
}),
|
|
|
|
)
|
|
|
|
}
|
2024-07-14 22:33:24 +00:00
|
|
|
HookRequest::Unrecognized(payload) => (
|
|
|
|
StatusCode::BAD_REQUEST,
|
|
|
|
Json(HookResponse {
|
|
|
|
ok: false,
|
|
|
|
message: Some(format!("unrecognized event type: {payload}")),
|
|
|
|
}),
|
|
|
|
),
|
|
|
|
}
|
2024-07-14 21:04:08 +00:00
|
|
|
}
|
|
|
|
|
2024-07-14 22:33:24 +00:00
|
|
|
#[derive(Debug)]
|
|
|
|
pub(crate) enum HookRequest {
|
|
|
|
Push(HookPush),
|
|
|
|
Unrecognized(String),
|
2024-07-14 21:04:08 +00:00
|
|
|
}
|
|
|
|
|
2024-07-14 22:33:24 +00:00
|
|
|
#[async_trait]
|
|
|
|
impl<S> FromRequest<S> for HookRequest
|
|
|
|
where
|
|
|
|
S: Send + Sync,
|
|
|
|
{
|
|
|
|
type Rejection = Response;
|
2024-07-14 21:04:08 +00:00
|
|
|
|
2024-07-14 22:33:24 +00:00
|
|
|
async fn from_request(req: Request, _state: &S) -> Result<Self, Self::Rejection> {
|
|
|
|
let event_type = req
|
|
|
|
.headers()
|
|
|
|
.get("X-Gitea-Event-Type")
|
|
|
|
.ok_or(StatusCode::UNSUPPORTED_MEDIA_TYPE.into_response())?;
|
|
|
|
let event_type = event_type
|
|
|
|
.to_str()
|
|
|
|
.map_err(|_| StatusCode::UNSUPPORTED_MEDIA_TYPE.into_response())?;
|
|
|
|
match event_type {
|
|
|
|
"push" => {
|
|
|
|
let Json(payload): Json<HookPush> =
|
|
|
|
req.extract().await.map_err(IntoResponse::into_response)?;
|
|
|
|
Ok(HookRequest::Push(payload))
|
|
|
|
}
|
|
|
|
_ => Ok(HookRequest::Unrecognized(event_type.to_owned())),
|
|
|
|
}
|
|
|
|
}
|
2024-07-14 20:38:07 +00:00
|
|
|
}
|
|
|
|
|
2024-07-14 22:33:24 +00:00
|
|
|
#[derive(Debug, Serialize)]
|
2024-07-14 20:13:06 +00:00
|
|
|
pub(crate) struct HookResponse {
|
|
|
|
ok: bool,
|
2024-07-14 22:33:24 +00:00
|
|
|
message: Option<String>,
|
2024-07-14 20:13:06 +00:00
|
|
|
}
|
2024-07-16 01:02:30 +00:00
|
|
|
|
|
|
|
pub(crate) async fn verify_signature(
|
|
|
|
request: Request,
|
|
|
|
next: Next,
|
|
|
|
) -> Result<impl IntoResponse, Response> {
|
|
|
|
let signature = request
|
|
|
|
.headers()
|
|
|
|
.get("X-Gitea-Signature")
|
|
|
|
.ok_or(StatusCode::BAD_REQUEST.into_response())?;
|
|
|
|
let signature = signature
|
|
|
|
.to_str()
|
|
|
|
.map_err(|_| StatusCode::BAD_REQUEST.into_response())?;
|
|
|
|
let signature = hex_to_bytes(signature).ok_or(StatusCode::BAD_REQUEST.into_response())?;
|
|
|
|
let secret = std::env::var("WEBHOOK_BRIDGE_HMAC_SECRET")
|
|
|
|
.map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response())?;
|
|
|
|
|
|
|
|
let request =
|
|
|
|
inspect_request_body(request, move |body| check_hash(body, secret, signature)).await?;
|
|
|
|
|
|
|
|
Ok(next.run(request).await)
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn inspect_request_body<F, Fut>(request: Request, inspector: F) -> Result<Request, Response>
|
|
|
|
where
|
|
|
|
F: FnOnce(Bytes) -> Fut,
|
|
|
|
Fut: Future<Output = Result<Bytes, Response>>,
|
|
|
|
{
|
|
|
|
let (parts, body) = request.into_parts();
|
|
|
|
|
|
|
|
let bytes = body
|
|
|
|
.collect()
|
|
|
|
.await
|
|
|
|
.map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response())?
|
|
|
|
.to_bytes();
|
|
|
|
|
|
|
|
let bytes = inspector(bytes).await?;
|
|
|
|
|
|
|
|
Ok(Request::from_parts(parts, Body::from(bytes)))
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn check_hash(body: Bytes, secret: String, signature: Vec<u8>) -> Result<Bytes, Response> {
|
|
|
|
tracing::info!("Checking signature {:02x?}", signature.as_slice());
|
|
|
|
tracing::info!("Using secret {:?}", secret);
|
|
|
|
tracing::info!("and body {}", general_purpose::STANDARD.encode(&body));
|
|
|
|
let mut mac = HmacSha256::new_from_slice(secret.as_bytes())
|
|
|
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response())?;
|
|
|
|
mac.update(&body);
|
|
|
|
mac.verify_slice(&signature)
|
|
|
|
.map_err(|e| (StatusCode::UNAUTHORIZED, e.to_string()).into_response())?;
|
|
|
|
Ok(body)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn hex_to_bytes(s: &str) -> Option<Vec<u8>> {
|
|
|
|
if s.len() % 2 == 0 {
|
|
|
|
(0..s.len())
|
|
|
|
.step_by(2)
|
|
|
|
.map(|i| {
|
|
|
|
s.get(i..i + 2)
|
|
|
|
.and_then(|sub| u8::from_str_radix(sub, 16).ok())
|
|
|
|
})
|
|
|
|
.collect()
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
}
|
2024-09-29 04:48:39 +00:00
|
|
|
|
|
|
|
pub(crate) async fn handle_push(
|
|
|
|
gitea: GiteaClient,
|
|
|
|
kubernetes_client: kube::Client,
|
|
|
|
webhook_payload: HookPush,
|
|
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
|
|
let repo_owner = webhook_payload.get_repo_owner()?;
|
|
|
|
let repo_name = webhook_payload.get_repo_name()?;
|
|
|
|
let pull_base_sha = webhook_payload.get_pull_base_sha()?;
|
|
|
|
let repo_tree = gitea.get_tree(repo_owner, repo_name, pull_base_sha).await?;
|
|
|
|
let remote_config = discover_webhook_bridge_config(&gitea, &repo_tree).await?;
|
|
|
|
let pipelines = discover_matching_push_triggers(
|
|
|
|
&gitea,
|
|
|
|
&repo_tree,
|
|
|
|
&webhook_payload.ref_field,
|
|
|
|
&remote_config,
|
|
|
|
)
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
run_pipelines(webhook_payload, pipelines, kubernetes_client).await?;
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|