Skip to main content

zlicenser_server/http/
webhook.rs

1use 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}