Add signature verification middleware.
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use axum::http::StatusCode;
|
||||
use axum::middleware;
|
||||
use axum::routing::get;
|
||||
use axum::routing::post;
|
||||
use axum::Json;
|
||||
@@ -15,6 +16,7 @@ use tracing_subscriber::layer::SubscriberExt;
|
||||
use tracing_subscriber::util::SubscriberInitExt;
|
||||
|
||||
use self::webhook::hook;
|
||||
use self::webhook::verify_signature;
|
||||
|
||||
mod hook_push;
|
||||
mod webhook;
|
||||
@@ -35,15 +37,16 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.expect("Set KUBECONFIG to a valid kubernetes config.");
|
||||
|
||||
let app = Router::new()
|
||||
.route("/health", get(health))
|
||||
.route("/hook", post(hook))
|
||||
.layer(middleware::from_fn(verify_signature))
|
||||
.route("/health", get(health))
|
||||
.layer((
|
||||
TraceLayer::new_for_http(),
|
||||
// Add a timeout layer so graceful shutdown can't wait forever.
|
||||
TimeoutLayer::new(Duration::from_secs(600)),
|
||||
));
|
||||
|
||||
let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await?;
|
||||
let listener = tokio::net::TcpListener::bind("0.0.0.0:9988").await?;
|
||||
tracing::info!("listening on {}", listener.local_addr().unwrap());
|
||||
axum::serve(listener, app)
|
||||
.with_graceful_shutdown(shutdown_signal())
|
||||
|
||||
@@ -1,17 +1,29 @@
|
||||
use std::future::Future;
|
||||
|
||||
use axum::async_trait;
|
||||
use axum::body::Body;
|
||||
use axum::body::Bytes;
|
||||
use axum::extract::FromRequest;
|
||||
use axum::extract::Request;
|
||||
use axum::http::HeaderMap;
|
||||
use axum::http::StatusCode;
|
||||
use axum::middleware::Next;
|
||||
use axum::response::IntoResponse;
|
||||
use axum::response::Response;
|
||||
use axum::Json;
|
||||
use axum::RequestExt;
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
use hmac::Hmac;
|
||||
use hmac::Mac;
|
||||
use http_body_util::BodyExt;
|
||||
use serde::Serialize;
|
||||
use sha2::Sha256;
|
||||
use tracing::debug;
|
||||
|
||||
use crate::hook_push::HookPush;
|
||||
|
||||
type HmacSha256 = Hmac<Sha256>;
|
||||
|
||||
pub(crate) async fn hook(
|
||||
_headers: HeaderMap,
|
||||
payload: HookRequest,
|
||||
@@ -72,3 +84,68 @@ pub(crate) struct HookResponse {
|
||||
ok: bool,
|
||||
message: Option<String>,
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user