zlicenser_server/http/
webhook.rs1use std::sync::Arc;
2
3use axum::{
4 body::Bytes,
5 extract::State,
6 http::{HeaderMap, StatusCode},
7 response::{IntoResponse, Response},
8};
9
10use crate::issuance::handlers::HandlerContext;
11use crate::storage::Storage;
12
13pub async fn stripe_handler<S: Storage + Clone + Send + Sync + 'static>(
14 State(ctx): State<Arc<HandlerContext<S>>>,
15 headers: HeaderMap,
16 body: Bytes,
17) -> Response {
18 let Some(secret) = ctx.config.stripe_webhook_secret.as_deref() else {
19 return StatusCode::NOT_IMPLEMENTED.into_response();
20 };
21
22 let Some(sig_header) = headers
23 .get("stripe-signature")
24 .and_then(|v| v.to_str().ok())
25 else {
26 return (StatusCode::BAD_REQUEST, "missing stripe-signature").into_response();
27 };
28
29 if !verify_stripe_signature(sig_header, &body, secret) {
30 return (StatusCode::UNAUTHORIZED, "invalid stripe-signature").into_response();
31 }
32
33 let event: serde_json::Value = match serde_json::from_slice(&body) {
34 Ok(v) => v,
35 Err(_) => return (StatusCode::BAD_REQUEST, "invalid json").into_response(),
36 };
37
38 let event_type = event["type"].as_str().unwrap_or("");
39 if event_type == "payment_intent.payment_failed" {
40 let intent_id = event["data"]["object"]["id"]
41 .as_str()
42 .unwrap_or_default()
43 .to_owned();
44 if !intent_id.is_empty() {
45 let storage = ctx.storage.clone();
46 tokio::spawn(async move {
47 if let Ok(Some(session)) = storage.get_session_by_payment_intent(&intent_id).await {
48 crate::issuance::handlers::abandon(
49 &storage,
50 session.id,
51 crate::storage::types::abandon_reason::PAYMENT_FAILED,
52 )
53 .await;
54 }
55 });
56 }
57 }
58
59 StatusCode::OK.into_response()
60}
61
62fn hmac_sha256(key: &[u8], data: &[u8]) -> [u8; 32] {
63 use sha2::{Digest, Sha256};
64 const BLOCK: usize = 64;
65 let mut k = [0u8; BLOCK];
66 if key.len() > BLOCK {
67 let h = Sha256::digest(key);
68 k[..32].copy_from_slice(&h);
69 } else {
70 k[..key.len()].copy_from_slice(key);
71 }
72 let mut ipad = [0u8; BLOCK];
73 let mut opad = [0u8; BLOCK];
74 for i in 0..BLOCK {
75 ipad[i] = k[i] ^ 0x36;
76 opad[i] = k[i] ^ 0x5c;
77 }
78 let inner: [u8; 32] = Sha256::new()
79 .chain_update(ipad)
80 .chain_update(data)
81 .finalize()
82 .into();
83 Sha256::new()
84 .chain_update(opad)
85 .chain_update(inner)
86 .finalize()
87 .into()
88}
89
90fn hex_to_bytes(s: &str) -> Option<Vec<u8>> {
91 if !s.len().is_multiple_of(2) {
92 return None;
93 }
94 (0..s.len())
95 .step_by(2)
96 .map(|i| u8::from_str_radix(&s[i..i + 2], 16).ok())
97 .collect()
98}
99
100fn verify_stripe_signature(header: &str, body: &[u8], secret: &str) -> bool {
101 let mut timestamp = None;
102 let mut signature_hex: Option<&str> = None;
103
104 for part in header.split(',') {
105 if let Some(t) = part.strip_prefix("t=") {
106 timestamp = Some(t);
107 } else if let Some(v) = part.strip_prefix("v1=") {
108 signature_hex = Some(v);
109 }
110 }
111
112 let (Some(ts), Some(sig_hex)) = (timestamp, signature_hex) else {
113 return false;
114 };
115
116 let Some(expected_bytes) = hex_to_bytes(sig_hex) else {
117 return false;
118 };
119
120 let signed_payload = format!("{}.{}", ts, String::from_utf8_lossy(body));
121 let computed = hmac_sha256(secret.as_bytes(), signed_payload.as_bytes());
122
123 computed.as_ref() == expected_bytes.as_slice()
124}