use crate::error::MppError;
use crate::protocol::core::{PaymentChallenge, PaymentCredential, Receipt};
pub struct ChallengeContext<'a, I> {
pub challenge: &'a PaymentChallenge,
pub input: &'a I,
pub error: Option<&'a str>,
}
pub struct ReceiptContext<'a, R> {
pub challenge_id: &'a str,
pub receipt: &'a Receipt,
pub response: R,
}
pub trait Transport: Send + Sync {
type Input;
type ChallengeOutput;
type ReceiptOutput;
fn name(&self) -> &str;
fn get_credential(&self, input: &Self::Input) -> Result<Option<PaymentCredential>, MppError>;
fn respond_challenge(&self, ctx: ChallengeContext<'_, Self::Input>) -> Self::ChallengeOutput;
fn respond_receipt(&self, ctx: ReceiptContext<'_, Self::ReceiptOutput>) -> Self::ReceiptOutput;
}
pub struct HttpTransport;
pub fn http() -> HttpTransport {
HttpTransport
}
impl Transport for HttpTransport {
type Input = http_types::Request<()>;
type ChallengeOutput = http_types::Response<String>;
type ReceiptOutput = http_types::Response<String>;
fn name(&self) -> &str {
"http"
}
fn get_credential(&self, input: &Self::Input) -> Result<Option<PaymentCredential>, MppError> {
let Some(header) = input.headers().get(http_types::header::AUTHORIZATION) else {
return Ok(None);
};
let header_str = header
.to_str()
.map_err(|e| MppError::MalformedCredential(Some(format!("invalid header: {e}"))))?;
let Some(payment) = crate::protocol::core::extract_payment_scheme(header_str) else {
return Ok(None);
};
let credential = crate::protocol::core::parse_authorization(payment).map_err(|e| {
MppError::MalformedCredential(Some(format!("failed to parse credential: {e}")))
})?;
Ok(Some(credential))
}
fn respond_challenge(&self, ctx: ChallengeContext<'_, Self::Input>) -> Self::ChallengeOutput {
let www_auth = crate::protocol::core::format_www_authenticate(ctx.challenge)
.unwrap_or_else(|_| "Payment".to_string());
let body = match ctx.error {
Some(msg) => serde_json::json!({ "error": msg }).to_string(),
None => serde_json::json!({ "error": "Payment Required" }).to_string(),
};
let mut resp = http_types::Response::builder()
.status(http_types::StatusCode::PAYMENT_REQUIRED)
.header(http_types::header::WWW_AUTHENTICATE, &www_auth)
.header(http_types::header::CONTENT_TYPE, "application/json")
.body(body)
.expect("response builder cannot fail");
resp.headers_mut().insert(
http_types::header::CACHE_CONTROL,
http_types::HeaderValue::from_static("no-store"),
);
resp
}
fn respond_receipt(&self, ctx: ReceiptContext<'_, Self::ReceiptOutput>) -> Self::ReceiptOutput {
let receipt_header =
crate::protocol::core::format_receipt(ctx.receipt).unwrap_or_else(|_| String::new());
let mut resp = ctx.response;
if let Ok(value) = http_types::HeaderValue::from_str(&receipt_header) {
resp.headers_mut()
.insert(crate::protocol::core::PAYMENT_RECEIPT_HEADER, value);
}
resp
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_http_transport_name() {
let transport = http();
assert_eq!(transport.name(), "http");
}
#[test]
fn test_http_get_credential_none() {
let transport = http();
let req = http_types::Request::builder()
.uri("/test")
.body(())
.unwrap();
let result = transport.get_credential(&req).unwrap();
assert!(result.is_none());
}
#[test]
fn test_http_get_credential_non_payment_auth() {
let transport = http();
let req = http_types::Request::builder()
.uri("/test")
.header("Authorization", "Bearer some-token")
.body(())
.unwrap();
let result = transport.get_credential(&req).unwrap();
assert!(result.is_none());
}
#[test]
fn test_http_get_credential_valid_payment() {
let transport = http();
let challenge = PaymentChallenge::new(
"test-id",
"test.example.com",
"tempo",
"charge",
crate::protocol::core::Base64UrlJson::from_value(
&serde_json::json!({"amount": "1000"}),
)
.unwrap(),
);
let credential = crate::protocol::core::PaymentCredential::new(
challenge.to_echo(),
crate::protocol::core::PaymentPayload::hash("0xdeadbeef"),
);
let auth_header = crate::protocol::core::format_authorization(&credential).unwrap();
let req = http_types::Request::builder()
.uri("/test")
.header("Authorization", &auth_header)
.body(())
.unwrap();
let result = transport.get_credential(&req).unwrap();
assert!(result.is_some(), "should parse valid Payment credential");
let parsed = result.unwrap();
assert_eq!(parsed.challenge.id, "test-id");
}
#[test]
fn test_http_respond_challenge() {
let transport = http();
let challenge = PaymentChallenge::new(
"test-id",
"test.example.com",
"tempo",
"charge",
crate::protocol::core::Base64UrlJson::from_value(
&serde_json::json!({"amount": "1000"}),
)
.unwrap(),
);
let req = http_types::Request::builder()
.uri("/test")
.body(())
.unwrap();
let resp = transport.respond_challenge(ChallengeContext {
challenge: &challenge,
input: &req,
error: None,
});
assert_eq!(resp.status(), http_types::StatusCode::PAYMENT_REQUIRED);
assert!(resp
.headers()
.get(http_types::header::WWW_AUTHENTICATE)
.is_some());
assert!(resp.body().contains("Payment Required"));
}
#[test]
fn test_http_respond_challenge_with_error() {
let transport = http();
let challenge = PaymentChallenge::new(
"test-id",
"test.example.com",
"tempo",
"charge",
crate::protocol::core::Base64UrlJson::from_value(
&serde_json::json!({"amount": "1000"}),
)
.unwrap(),
);
let req = http_types::Request::builder()
.uri("/test")
.body(())
.unwrap();
let resp = transport.respond_challenge(ChallengeContext {
challenge: &challenge,
input: &req,
error: Some("Verification failed"),
});
assert_eq!(resp.status(), http_types::StatusCode::PAYMENT_REQUIRED);
assert!(resp.body().contains("Verification failed"));
}
#[test]
fn test_http_respond_receipt() {
let transport = http();
let receipt = Receipt::success("tempo", "0xabc123");
let resp = http_types::Response::builder()
.status(http_types::StatusCode::OK)
.body("ok".to_string())
.unwrap();
let resp = transport.respond_receipt(ReceiptContext {
challenge_id: "ch-1",
receipt: &receipt,
response: resp,
});
assert_eq!(resp.status(), http_types::StatusCode::OK);
assert!(resp
.headers()
.get(crate::protocol::core::PAYMENT_RECEIPT_HEADER)
.is_some());
}
}