1use std::convert::Infallible;
8use std::sync::Arc;
9
10use axum_core::body::Body;
11use axum_core::extract::Request;
12use axum_core::response::{IntoResponse, Response};
13use http::{HeaderMap, HeaderValue, StatusCode};
14use r402::facilitator::Facilitator;
15use r402::proto;
16use r402::proto::Base64Bytes;
17use r402::proto::v2;
18use serde_json::json;
19use tower::Service;
20#[cfg(feature = "telemetry")]
21use tracing::{Instrument, instrument};
22use url::Url;
23
24use super::{PaygateError, VerificationError};
25
26#[derive(Debug, Clone)]
28pub struct ResourceInfoBuilder {
29 pub description: String,
31 pub mime_type: String,
33 pub url: Option<String>,
35}
36
37impl Default for ResourceInfoBuilder {
38 fn default() -> Self {
39 Self {
40 description: String::new(),
41 mime_type: "application/json".to_string(),
42 url: None,
43 }
44 }
45}
46
47impl ResourceInfoBuilder {
48 #[allow(clippy::unwrap_used)]
57 pub fn as_resource_info(&self, base_url: Option<&Url>, req: &Request) -> v2::ResourceInfo {
58 let url = self.url.clone().unwrap_or_else(|| {
59 let mut url = base_url.cloned().unwrap_or_else(|| {
60 let host = req.headers().get("host").and_then(|h| h.to_str().ok()).unwrap_or("localhost");
61 let origin = format!("http://{host}");
62 let url = Url::parse(&origin).unwrap_or_else(|_| Url::parse("http://localhost").unwrap());
63 #[cfg(feature = "telemetry")]
64 tracing::warn!(
65 "X402Middleware base_url is not configured; using {url} as origin for resource resolution"
66 );
67 url
68 });
69 let request_uri = req.uri();
70 url.set_path(request_uri.path());
71 url.set_query(request_uri.query());
72 url.to_string()
73 });
74 v2::ResourceInfo {
75 description: self.description.clone(),
76 mime_type: self.mime_type.clone(),
77 url,
78 }
79 }
80}
81
82#[allow(missing_debug_implementations)]
93pub struct Paygate<TFacilitator> {
94 pub(crate) facilitator: TFacilitator,
95 pub(crate) settle_before_execution: bool,
96 pub(crate) accepts: Arc<Vec<v2::PriceTag>>,
97 pub(crate) resource: v2::ResourceInfo,
98}
99
100#[allow(missing_debug_implementations)]
112pub struct PaygateBuilder<TFacilitator> {
113 facilitator: TFacilitator,
114 settle_before_execution: bool,
115 accepts: Vec<v2::PriceTag>,
116 resource: Option<v2::ResourceInfo>,
117}
118
119impl<TFacilitator> Paygate<TFacilitator> {
120 pub const fn builder(facilitator: TFacilitator) -> PaygateBuilder<TFacilitator> {
122 PaygateBuilder {
123 facilitator,
124 settle_before_execution: false,
125 accepts: Vec::new(),
126 resource: None,
127 }
128 }
129
130 pub fn accepts(&self) -> &[v2::PriceTag] {
132 &self.accepts
133 }
134
135 pub const fn resource(&self) -> &v2::ResourceInfo {
137 &self.resource
138 }
139}
140
141impl<TFacilitator> PaygateBuilder<TFacilitator> {
142 #[must_use]
144 pub fn accept(mut self, price_tag: v2::PriceTag) -> Self {
145 self.accepts.push(price_tag);
146 self
147 }
148
149 #[must_use]
151 pub fn accepts(mut self, price_tags: impl IntoIterator<Item = v2::PriceTag>) -> Self {
152 self.accepts.extend(price_tags);
153 self
154 }
155
156 #[must_use]
158 pub fn resource(mut self, resource: v2::ResourceInfo) -> Self {
159 self.resource = Some(resource);
160 self
161 }
162
163 #[must_use]
167 pub const fn settle_before_execution(mut self, enabled: bool) -> Self {
168 self.settle_before_execution = enabled;
169 self
170 }
171
172 pub fn build(self) -> Paygate<TFacilitator> {
176 Paygate {
177 facilitator: self.facilitator,
178 settle_before_execution: self.settle_before_execution,
179 accepts: Arc::new(self.accepts),
180 resource: self.resource.unwrap_or_else(|| v2::ResourceInfo {
181 description: String::new(),
182 mime_type: "application/json".to_owned(),
183 url: String::new(),
184 }),
185 }
186 }
187}
188
189const PAYMENT_HEADER_NAME: &str = "Payment-Signature";
191
192type V2PaymentPayload = v2::PaymentPayload<v2::PaymentRequirements, serde_json::Value>;
194
195impl<TFacilitator> Paygate<TFacilitator> {
196 async fn call_inner<
198 ReqBody,
199 ResBody,
200 S: Service<http::Request<ReqBody>, Response = http::Response<ResBody>>,
201 >(
202 mut inner: S,
203 req: http::Request<ReqBody>,
204 ) -> Result<http::Response<ResBody>, S::Error>
205 where
206 S::Future: Send,
207 {
208 #[cfg(feature = "telemetry")]
209 {
210 inner
211 .call(req)
212 .instrument(tracing::info_span!("inner"))
213 .await
214 }
215 #[cfg(not(feature = "telemetry"))]
216 {
217 inner.call(req).await
218 }
219 }
220}
221
222impl<TFacilitator> Paygate<TFacilitator>
223where
224 TFacilitator: Facilitator + Sync,
225{
226 #[cfg_attr(
235 feature = "telemetry",
236 instrument(name = "x402.handle_request", skip_all)
237 )]
238 pub async fn handle_request<
239 ReqBody,
240 ResBody,
241 S: Service<http::Request<ReqBody>, Response = http::Response<ResBody>>,
242 >(
243 self,
244 inner: S,
245 req: http::Request<ReqBody>,
246 ) -> Result<Response, Infallible>
247 where
248 S::Response: IntoResponse,
249 S::Error: IntoResponse,
250 S::Future: Send,
251 {
252 match self.handle_request_fallible(inner, req).await {
253 Ok(response) => Ok(response),
254 Err(err) => Ok(error_into_response(err, &self.accepts, &self.resource)),
255 }
256 }
257
258 pub async fn enrich_accepts(&mut self) {
260 let capabilities = self.facilitator.supported().await.unwrap_or_default();
261
262 let accepts = (*self.accepts)
263 .clone()
264 .into_iter()
265 .map(|mut pt| {
266 pt.enrich(&capabilities);
267 pt
268 })
269 .collect::<Vec<_>>();
270 self.accepts = Arc::new(accepts);
271 }
272
273 pub async fn handle_request_fallible<
282 ReqBody,
283 ResBody,
284 S: Service<http::Request<ReqBody>, Response = http::Response<ResBody>>,
285 >(
286 &self,
287 inner: S,
288 req: http::Request<ReqBody>,
289 ) -> Result<Response, PaygateError>
290 where
291 S::Response: IntoResponse,
292 S::Error: IntoResponse,
293 S::Future: Send,
294 {
295 let header = extract_payment_header(req.headers(), PAYMENT_HEADER_NAME).ok_or(
296 VerificationError::PaymentHeaderRequired(PAYMENT_HEADER_NAME),
297 )?;
298 let payment_payload = extract_payment_payload::<V2PaymentPayload>(header)
299 .ok_or(VerificationError::InvalidPaymentHeader)?;
300
301 let verify_request = make_verify_request(payment_payload, &self.accepts)?;
302
303 if self.settle_before_execution {
304 #[cfg(feature = "telemetry")]
305 tracing::debug!("Settling payment before request execution");
306
307 let settlement = self
308 .facilitator
309 .settle(verify_request.into())
310 .await
311 .map_err(|e| PaygateError::Settlement(format!("{e}")))?;
312
313 if let proto::SettleResponse::Error {
314 reason, message, ..
315 } = &settlement
316 {
317 let detail = message.as_deref().unwrap_or(reason.as_str());
318 return Err(PaygateError::Settlement(detail.to_owned()));
319 }
320
321 let header_value = settlement_to_header(settlement)?;
322
323 let response = match Self::call_inner(inner, req).await {
324 Ok(response) => response,
325 Err(err) => return Ok(err.into_response()),
326 };
327
328 let mut res = response;
329 res.headers_mut().insert("Payment-Response", header_value);
330 Ok(res.into_response())
331 } else {
332 #[cfg(feature = "telemetry")]
333 tracing::debug!("Settling payment after request execution");
334
335 let verify_response = self
336 .facilitator
337 .verify(verify_request.clone())
338 .await
339 .map_err(|e| VerificationError::VerificationFailed(format!("{e}")))?;
340
341 validate_verify_response(verify_response)?;
342
343 let response = match Self::call_inner(inner, req).await {
344 Ok(response) => response,
345 Err(err) => return Ok(err.into_response()),
346 };
347
348 if response.status().is_client_error() || response.status().is_server_error() {
349 return Ok(response.into_response());
350 }
351
352 let settlement = self
353 .facilitator
354 .settle(verify_request.into())
355 .await
356 .map_err(|e| PaygateError::Settlement(format!("{e}")))?;
357
358 if let proto::SettleResponse::Error {
359 reason, message, ..
360 } = &settlement
361 {
362 let detail = message.as_deref().unwrap_or(reason.as_str());
363 return Err(PaygateError::Settlement(detail.to_owned()));
364 }
365
366 let header_value = settlement_to_header(settlement)?;
367
368 let mut res = response;
369 res.headers_mut().insert("Payment-Response", header_value);
370 Ok(res.into_response())
371 }
372 }
373}
374
375fn extract_payment_header<'a>(header_map: &'a HeaderMap, header_name: &'a str) -> Option<&'a [u8]> {
377 header_map.get(header_name).map(HeaderValue::as_bytes)
378}
379
380fn extract_payment_payload<T>(header_bytes: &[u8]) -> Option<T>
382where
383 T: serde::de::DeserializeOwned,
384{
385 let base64 = Base64Bytes::from(header_bytes).decode().ok()?;
386 let value = serde_json::from_slice(base64.as_ref()).ok()?;
387 Some(value)
388}
389
390#[allow(clippy::needless_pass_by_value)] fn settlement_to_header(settlement: proto::SettleResponse) -> Result<HeaderValue, PaygateError> {
395 let json =
396 serde_json::to_vec(&settlement).map_err(|err| PaygateError::Settlement(err.to_string()))?;
397 let payment_header = Base64Bytes::encode(json);
398 HeaderValue::from_bytes(payment_header.as_ref())
399 .map_err(|err| PaygateError::Settlement(err.to_string()))
400}
401
402fn make_verify_request(
404 payment_payload: V2PaymentPayload,
405 accepts: &[v2::PriceTag],
406) -> Result<proto::VerifyRequest, VerificationError> {
407 let accepted = &payment_payload.accepted;
408
409 let selected = accepts
410 .iter()
411 .find(|price_tag| **price_tag == *accepted)
412 .ok_or(VerificationError::NoPaymentMatching)?;
413
414 let verify_request = v2::VerifyRequest {
415 x402_version: v2::V2,
416 payment_payload,
417 payment_requirements: selected.requirements.clone(),
418 };
419
420 let json = serde_json::to_value(&verify_request)
421 .map_err(|e| VerificationError::VerificationFailed(format!("{e}")))?;
422
423 Ok(proto::VerifyRequest::from(json))
424}
425
426fn validate_verify_response(
428 verify_response: proto::VerifyResponse,
429) -> Result<(), VerificationError> {
430 match verify_response {
431 proto::VerifyResponse::Valid { .. } => Ok(()),
432 proto::VerifyResponse::Invalid { reason, .. } => {
433 Err(VerificationError::VerificationFailed(reason))
434 }
435 _ => Err(VerificationError::VerificationFailed(
436 "unknown verify response variant".into(),
437 )),
438 }
439}
440
441fn error_into_response(
443 err: PaygateError,
444 accepts: &[v2::PriceTag],
445 resource: &v2::ResourceInfo,
446) -> Response {
447 match err {
448 PaygateError::Verification(err) => {
449 let payment_required_response = v2::PaymentRequired {
450 error: Some(err.to_string()),
451 accepts: accepts.iter().map(|pt| pt.requirements.clone()).collect(),
452 x402_version: v2::V2,
453 resource: resource.clone(),
454 extensions: None,
455 };
456 let payment_required_bytes =
457 serde_json::to_vec(&payment_required_response).expect("serialization failed");
458 let payment_required_header = Base64Bytes::encode(&payment_required_bytes);
459 let header_value = HeaderValue::from_bytes(payment_required_header.as_ref())
460 .expect("Failed to create header value");
461
462 Response::builder()
463 .status(StatusCode::PAYMENT_REQUIRED)
464 .header("Payment-Required", header_value)
465 .body(Body::empty())
466 .expect("Fail to construct response")
467 }
468 PaygateError::Settlement(ref err) => {
469 #[cfg(feature = "telemetry")]
470 tracing::error!(details = %err, "Settlement failed");
471 let body = Body::from(
472 json!({ "error": "Settlement failed" }).to_string(),
473 );
474 Response::builder()
475 .status(StatusCode::INTERNAL_SERVER_ERROR)
476 .header("Content-Type", "application/json")
477 .body(body)
478 .expect("Fail to construct response")
479 }
480 }
481}