use std::sync::Arc;
use warp::Filter;
use mpp::protocol::core::extract_payment_scheme;
use super::state::MppState;
pub struct MppPaymentContext {
pub receipt: mpp::Receipt,
pub management_response: Option<serde_json::Value>,
pub channel_id: String,
pub backend_key: String,
}
#[derive(Debug)]
pub struct MppChallenge {
pub www_authenticate: String,
}
impl warp::reject::Reject for MppChallenge {}
#[derive(Debug)]
pub struct MppVerificationFailed {
pub message: String,
}
impl warp::reject::Reject for MppVerificationFailed {}
pub fn mpp_payment_filter(
state: Arc<MppState>,
chain: Option<String>,
) -> impl Filter<Extract = (MppPaymentContext,), Error = warp::Rejection> + Clone {
warp::header::optional::<String>("authorization")
.and(warp::any().map(move || state.clone()))
.and(warp::any().map(move || chain.clone()))
.and_then(
|auth_header: Option<String>, state: Arc<MppState>, chain: Option<String>| async move {
verify_payment_impl(auth_header, &state, chain).await
},
)
}
pub(crate) async fn verify_payment_impl(
auth_header: Option<String>,
state: &MppState,
chain: Option<String>,
) -> Result<MppPaymentContext, warp::Rejection> {
let auth_value = match auth_header.as_deref() {
Some(h) if extract_payment_scheme(h).is_some() => h,
_ => {
let challenges = match chain.as_deref() {
Some(c) => {
let opts = mpp::server::SessionChallengeOptions {
unit_type: Some("token"),
..Default::default()
};
match state.session_challenge(Some(c), "1", opts) {
Ok(ch) => vec![ch],
Err(_) => state.all_session_challenges(
"1",
mpp::server::SessionChallengeOptions {
unit_type: Some("token"),
..Default::default()
},
),
}
}
None => state.all_session_challenges(
"1",
mpp::server::SessionChallengeOptions {
unit_type: Some("token"),
..Default::default()
},
),
};
if challenges.is_empty() {
return Err(warp::reject::custom(MppVerificationFailed {
message: "no MPP backends available".to_string(),
}));
}
let mut parts = Vec::with_capacity(challenges.len());
for ch in &challenges {
let formatted = mpp::format_www_authenticate(ch).map_err(|e| {
warp::reject::custom(MppVerificationFailed {
message: format!("failed to format challenge: {e}"),
})
})?;
parts.push(formatted);
}
let www_authenticate = parts.join(", ");
return Err(warp::reject::custom(MppChallenge { www_authenticate }));
}
};
let credential = mpp::parse_authorization(auth_value).map_err(|e| {
warp::reject::custom(MppVerificationFailed {
message: format!("invalid payment credential: {e}"),
})
})?;
let (backend_key, result) = state.verify_session(&credential).await.map_err(|e| {
warp::reject::custom(MppVerificationFailed {
message: e.to_string(),
})
})?;
Ok(MppPaymentContext {
channel_id: result.receipt.reference.clone(),
receipt: result.receipt,
management_response: result.management_response,
backend_key,
})
}
pub async fn verify_mpp_payment(
state: Arc<MppState>,
chain: Option<String>,
auth_header: Option<String>,
) -> Result<MppPaymentContext, warp::Rejection> {
verify_payment_impl(auth_header, &state, chain).await
}
pub fn handle_mpp_rejection(err: &warp::Rejection) -> Option<warp::http::Response<String>> {
use warp::http::StatusCode;
if let Some(challenge) = err.find::<MppChallenge>() {
let body = serde_json::json!({
"error": {
"message": "payment required",
"code": 402
}
})
.to_string();
let response = warp::http::Response::builder()
.status(StatusCode::PAYMENT_REQUIRED)
.header("content-type", "application/json")
.header("www-authenticate", &challenge.www_authenticate)
.body(body)
.ok()?;
return Some(response);
}
if let Some(failed) = err.find::<MppVerificationFailed>() {
let body = serde_json::json!({
"error": {
"message": failed.message,
"code": 402
}
})
.to_string();
let response = warp::http::Response::builder()
.status(StatusCode::PAYMENT_REQUIRED)
.header("content-type", "application/json")
.body(body)
.ok()?;
return Some(response);
}
None
}