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) accepts: Arc<Vec<v2::PriceTag>>,
96 pub(crate) resource: v2::ResourceInfo,
97}
98
99#[allow(missing_debug_implementations)]
110pub struct PaygateBuilder<TFacilitator> {
111 facilitator: TFacilitator,
112 accepts: Vec<v2::PriceTag>,
113 resource: Option<v2::ResourceInfo>,
114}
115
116impl<TFacilitator> Paygate<TFacilitator> {
117 pub const fn builder(facilitator: TFacilitator) -> PaygateBuilder<TFacilitator> {
119 PaygateBuilder {
120 facilitator,
121 accepts: Vec::new(),
122 resource: None,
123 }
124 }
125
126 pub fn accepts(&self) -> &[v2::PriceTag] {
128 &self.accepts
129 }
130
131 pub const fn resource(&self) -> &v2::ResourceInfo {
133 &self.resource
134 }
135}
136
137impl<TFacilitator> PaygateBuilder<TFacilitator> {
138 #[must_use]
140 pub fn accept(mut self, price_tag: v2::PriceTag) -> Self {
141 self.accepts.push(price_tag);
142 self
143 }
144
145 #[must_use]
147 pub fn accepts(mut self, price_tags: impl IntoIterator<Item = v2::PriceTag>) -> Self {
148 self.accepts.extend(price_tags);
149 self
150 }
151
152 #[must_use]
154 pub fn resource(mut self, resource: v2::ResourceInfo) -> Self {
155 self.resource = Some(resource);
156 self
157 }
158
159 pub fn build(self) -> Paygate<TFacilitator> {
163 Paygate {
164 facilitator: self.facilitator,
165 accepts: Arc::new(self.accepts),
166 resource: self.resource.unwrap_or_else(|| v2::ResourceInfo {
167 description: String::new(),
168 mime_type: "application/json".to_owned(),
169 url: String::new(),
170 }),
171 }
172 }
173}
174
175const PAYMENT_HEADER_NAME: &str = "Payment-Signature";
177
178type V2PaymentPayload = v2::PaymentPayload<v2::PaymentRequirements, serde_json::Value>;
180
181impl<TFacilitator> Paygate<TFacilitator> {
182 async fn call_inner<
184 ReqBody,
185 ResBody,
186 S: Service<http::Request<ReqBody>, Response = http::Response<ResBody>>,
187 >(
188 mut inner: S,
189 req: http::Request<ReqBody>,
190 ) -> Result<http::Response<ResBody>, S::Error>
191 where
192 S::Future: Send,
193 {
194 #[cfg(feature = "telemetry")]
195 {
196 inner
197 .call(req)
198 .instrument(tracing::info_span!("inner"))
199 .await
200 }
201 #[cfg(not(feature = "telemetry"))]
202 {
203 inner.call(req).await
204 }
205 }
206}
207
208impl<TFacilitator> Paygate<TFacilitator>
209where
210 TFacilitator: Facilitator + Sync,
211{
212 #[cfg_attr(
221 feature = "telemetry",
222 instrument(name = "x402.handle_request", skip_all)
223 )]
224 pub async fn handle_request<
225 ReqBody,
226 ResBody,
227 S: Service<http::Request<ReqBody>, Response = http::Response<ResBody>>,
228 >(
229 self,
230 inner: S,
231 req: http::Request<ReqBody>,
232 ) -> Result<Response, Infallible>
233 where
234 S::Response: IntoResponse,
235 S::Error: IntoResponse,
236 S::Future: Send,
237 {
238 match self.handle_request_fallible(inner, req).await {
239 Ok(response) => Ok(response),
240 Err(err) => Ok(error_into_response(err, &self.accepts, &self.resource)),
241 }
242 }
243
244 pub async fn enrich_accepts(&mut self) {
246 let capabilities = self.facilitator.supported().await.unwrap_or_default();
247
248 let accepts = (*self.accepts)
249 .clone()
250 .into_iter()
251 .map(|mut pt| {
252 pt.enrich(&capabilities);
253 pt
254 })
255 .collect::<Vec<_>>();
256 self.accepts = Arc::new(accepts);
257 }
258
259 pub async fn handle_request_fallible<
268 ReqBody,
269 ResBody,
270 S: Service<http::Request<ReqBody>, Response = http::Response<ResBody>>,
271 >(
272 &self,
273 inner: S,
274 req: http::Request<ReqBody>,
275 ) -> Result<Response, PaygateError>
276 where
277 S::Response: IntoResponse,
278 S::Error: IntoResponse,
279 S::Future: Send,
280 {
281 let header = extract_payment_header(req.headers(), PAYMENT_HEADER_NAME).ok_or(
282 VerificationError::PaymentHeaderRequired(PAYMENT_HEADER_NAME),
283 )?;
284 let payment_payload = extract_payment_payload::<V2PaymentPayload>(header)
285 .ok_or(VerificationError::InvalidPaymentHeader)?;
286
287 let verify_request = make_verify_request(payment_payload, &self.accepts)?;
288
289 let verify_response = self
291 .facilitator
292 .verify(verify_request.clone())
293 .await
294 .map_err(|e| VerificationError::VerificationFailed(format!("{e}")))?;
295
296 validate_verify_response(verify_response)?;
297
298 let response = match Self::call_inner(inner, req).await {
300 Ok(response) => response,
301 Err(err) => return Ok(err.into_response()),
302 };
303
304 if response.status().is_client_error() || response.status().is_server_error() {
306 return Ok(response.into_response());
307 }
308
309 let settlement = self
311 .facilitator
312 .settle(verify_request.into())
313 .await
314 .map_err(|e| PaygateError::Settlement(format!("{e}")))?;
315
316 if let proto::SettleResponse::Error {
317 reason, message, ..
318 } = &settlement
319 {
320 let detail = message.as_deref().unwrap_or(reason.as_str());
321 return Err(PaygateError::Settlement(detail.to_owned()));
322 }
323
324 let header_value = settlement_to_header(settlement)?;
325
326 let mut res = response;
327 res.headers_mut().insert("Payment-Response", header_value);
328 Ok(res.into_response())
329 }
330}
331
332fn extract_payment_header<'a>(header_map: &'a HeaderMap, header_name: &'a str) -> Option<&'a [u8]> {
334 header_map.get(header_name).map(HeaderValue::as_bytes)
335}
336
337fn extract_payment_payload<T>(header_bytes: &[u8]) -> Option<T>
339where
340 T: serde::de::DeserializeOwned,
341{
342 let base64 = Base64Bytes::from(header_bytes).decode().ok()?;
343 let value = serde_json::from_slice(base64.as_ref()).ok()?;
344 Some(value)
345}
346
347#[allow(clippy::needless_pass_by_value)] fn settlement_to_header(settlement: proto::SettleResponse) -> Result<HeaderValue, PaygateError> {
352 let json =
353 serde_json::to_vec(&settlement).map_err(|err| PaygateError::Settlement(err.to_string()))?;
354 let payment_header = Base64Bytes::encode(json);
355 HeaderValue::from_bytes(payment_header.as_ref())
356 .map_err(|err| PaygateError::Settlement(err.to_string()))
357}
358
359fn make_verify_request(
361 payment_payload: V2PaymentPayload,
362 accepts: &[v2::PriceTag],
363) -> Result<proto::VerifyRequest, VerificationError> {
364 let accepted = &payment_payload.accepted;
365
366 let selected = accepts
367 .iter()
368 .find(|price_tag| **price_tag == *accepted)
369 .ok_or(VerificationError::NoPaymentMatching)?;
370
371 let verify_request = v2::VerifyRequest {
372 x402_version: v2::V2,
373 payment_payload,
374 payment_requirements: selected.requirements.clone(),
375 };
376
377 let json = serde_json::to_value(&verify_request)
378 .map_err(|e| VerificationError::VerificationFailed(format!("{e}")))?;
379
380 Ok(proto::VerifyRequest::from(json))
381}
382
383fn validate_verify_response(
385 verify_response: proto::VerifyResponse,
386) -> Result<(), VerificationError> {
387 match verify_response {
388 proto::VerifyResponse::Valid { .. } => Ok(()),
389 proto::VerifyResponse::Invalid { reason, .. } => {
390 Err(VerificationError::VerificationFailed(reason))
391 }
392 _ => Err(VerificationError::VerificationFailed(
393 "unknown verify response variant".into(),
394 )),
395 }
396}
397
398fn error_into_response(
400 err: PaygateError,
401 accepts: &[v2::PriceTag],
402 resource: &v2::ResourceInfo,
403) -> Response {
404 match err {
405 PaygateError::Verification(err) => {
406 let payment_required_response = v2::PaymentRequired {
407 error: Some(err.to_string()),
408 accepts: accepts.iter().map(|pt| pt.requirements.clone()).collect(),
409 x402_version: v2::V2,
410 resource: resource.clone(),
411 extensions: None,
412 };
413 let payment_required_bytes =
414 serde_json::to_vec(&payment_required_response).expect("serialization failed");
415 let payment_required_header = Base64Bytes::encode(&payment_required_bytes);
416 let header_value = HeaderValue::from_bytes(payment_required_header.as_ref())
417 .expect("Failed to create header value");
418
419 Response::builder()
420 .status(StatusCode::PAYMENT_REQUIRED)
421 .header("Payment-Required", header_value)
422 .header("Content-Type", "application/json")
423 .body(Body::from(payment_required_bytes))
424 .expect("Fail to construct response")
425 }
426 PaygateError::Settlement(ref err) => {
427 #[cfg(feature = "telemetry")]
428 tracing::error!(details = %err, "Settlement failed");
429 let body = Body::from(
430 json!({ "error": "Settlement failed", "details": err.clone() }).to_string(),
431 );
432 Response::builder()
433 .status(StatusCode::PAYMENT_REQUIRED)
434 .header("Content-Type", "application/json")
435 .body(body)
436 .expect("Fail to construct response")
437 }
438 }
439}