1mod admin;
35pub use admin::admin;
36
37use axum::{
38 async_trait,
39 extract::{FromRequest, Request},
40 http::StatusCode,
41 response::{IntoResponse, Response},
42};
43use webhooksmith::signing;
44use serde::de::DeserializeOwned;
45use tower_layer::Layer;
46use std::sync::Arc;
47
48const MAX_BODY_BYTES: usize = 1_048_576; #[derive(Clone)]
54struct WebhookSecret(Arc<String>);
55
56#[derive(Clone)]
70pub struct WebhookSecretLayer {
71 secret: Arc<String>,
72}
73
74impl WebhookSecretLayer {
75 pub fn new(secret: impl Into<String>) -> Self {
76 Self { secret: Arc::new(secret.into()) }
77 }
78}
79
80impl<S> Layer<S> for WebhookSecretLayer {
81 type Service = WebhookSecretService<S>;
82
83 fn layer(&self, inner: S) -> Self::Service {
84 WebhookSecretService {
85 inner,
86 secret: self.secret.clone(),
87 }
88 }
89}
90
91#[derive(Clone)]
93pub struct WebhookSecretService<S> {
94 inner: S,
95 secret: Arc<String>,
96}
97
98impl<S, B> tower::Service<Request<B>> for WebhookSecretService<S>
99where
100 S: tower::Service<Request<B>>,
101{
102 type Response = S::Response;
103 type Error = S::Error;
104 type Future = S::Future;
105
106 fn poll_ready(
107 &mut self,
108 cx: &mut std::task::Context<'_>,
109 ) -> std::task::Poll<Result<(), Self::Error>> {
110 self.inner.poll_ready(cx)
111 }
112
113 fn call(&mut self, mut req: Request<B>) -> Self::Future {
114 req.extensions_mut()
115 .insert(WebhookSecret(self.secret.clone()));
116 self.inner.call(req)
117 }
118}
119
120#[derive(Debug)]
124pub enum WebhookRejection {
125 MissingSecret,
126 MissingTimestamp,
127 MissingSignature,
128 BodyTooLarge,
129 InvalidSignature,
130 InvalidBody(serde_json::Error),
131}
132
133impl IntoResponse for WebhookRejection {
134 fn into_response(self) -> Response {
135 let (status, msg) = match &self {
136 Self::MissingSecret => (StatusCode::INTERNAL_SERVER_ERROR, "webhook secret not configured"),
137 Self::MissingTimestamp => (StatusCode::BAD_REQUEST, "missing x-hooksmith-timestamp header"),
138 Self::MissingSignature => (StatusCode::UNAUTHORIZED, "missing x-hooksmith-signature header"),
139 Self::BodyTooLarge => (StatusCode::PAYLOAD_TOO_LARGE, "request body too large"),
140 Self::InvalidSignature => (StatusCode::UNAUTHORIZED, "invalid webhook signature"),
141 Self::InvalidBody(_) => (StatusCode::UNPROCESSABLE_ENTITY, "invalid JSON body"),
142 };
143 (status, msg).into_response()
144 }
145}
146
147pub struct WebhookPayload {
151 pub event_type: String,
152 pub event_id: Option<String>,
153 pub timestamp: i64,
154 pub body: serde_json::Value,
155}
156
157async fn extract_and_verify(req: Request) -> Result<WebhookPayload, WebhookRejection> {
158 let secret = req
160 .extensions()
161 .get::<WebhookSecret>()
162 .ok_or(WebhookRejection::MissingSecret)?
163 .0
164 .clone();
165
166 let timestamp: i64 = req
168 .headers()
169 .get("x-hooksmith-timestamp")
170 .and_then(|v| v.to_str().ok())
171 .and_then(|v| v.parse().ok())
172 .ok_or(WebhookRejection::MissingTimestamp)?;
173
174 let signature = req
175 .headers()
176 .get("x-hooksmith-signature")
177 .and_then(|v| v.to_str().ok())
178 .ok_or(WebhookRejection::MissingSignature)?
179 .to_owned();
180
181 let event_type = req
182 .headers()
183 .get("x-hooksmith-event-type")
184 .and_then(|v| v.to_str().ok())
185 .unwrap_or("unknown")
186 .to_owned();
187
188 let event_id = req
189 .headers()
190 .get("x-hooksmith-event-id")
191 .and_then(|v| v.to_str().ok())
192 .map(|s| s.to_owned());
193
194 let bytes = axum::body::to_bytes(req.into_body(), MAX_BODY_BYTES)
196 .await
197 .map_err(|_| WebhookRejection::BodyTooLarge)?;
198
199 if !signing::verify(&secret, timestamp, &bytes, &signature) {
201 tracing::warn!(
202 event_type = %event_type,
203 "webhook signature verification failed"
204 );
205 return Err(WebhookRejection::InvalidSignature);
206 }
207
208 let body: serde_json::Value =
209 serde_json::from_slice(&bytes).map_err(WebhookRejection::InvalidBody)?;
210
211 Ok(WebhookPayload { event_type, event_id, timestamp, body })
212}
213
214pub struct VerifiedWebhook(pub WebhookPayload);
223
224#[async_trait]
225impl<S> FromRequest<S> for VerifiedWebhook
226where
227 S: Send + Sync,
228{
229 type Rejection = WebhookRejection;
230
231 async fn from_request(req: Request, _state: &S) -> Result<Self, Self::Rejection> {
232 Ok(Self(extract_and_verify(req).await?))
233 }
234}
235
236pub struct TypedWebhook<T>(pub T);
243
244#[async_trait]
245impl<S, T> FromRequest<S> for TypedWebhook<T>
246where
247 S: Send + Sync,
248 T: DeserializeOwned,
249{
250 type Rejection = WebhookRejection;
251
252 async fn from_request(req: Request, _state: &S) -> Result<Self, Self::Rejection> {
253 let payload = extract_and_verify(req).await?;
254 let typed: T =
255 serde_json::from_value(payload.body).map_err(WebhookRejection::InvalidBody)?;
256 Ok(Self(typed))
257 }
258}