1use std::convert::Infallible;
4use std::future::Future;
5use std::pin::Pin;
6use std::sync::Arc;
7use std::task::{Context, Poll};
8
9use axum_core::extract::Request;
10use axum_core::response::Response;
11use r402_protocol::extension::Extension;
12use r402_protocol::network::ChainIdPattern;
13use r402_protocol::payment::{PriceTag, ResourceInfo};
14use r402_server::{BackgroundSettlementTracker, ResourceServer, SchemeNetworkServer};
15use tower::util::BoxCloneSyncService;
16use tower::{Layer, Service};
17use url::Url;
18
19use super::SettlementMode;
20use super::builder::{BuildError, validate_static_layer};
21use super::fail::abort_response;
22use super::gate::Gate;
23use super::hooks::{DynGateHooks, GateHooks, ProtectedRequestOutcome};
24use super::pricing::{DynamicPriceTags, PriceTagSource, StaticPriceTags};
25
26#[derive(Debug, Clone)]
28pub(crate) struct ResourceTemplate {
29 pub(crate) description: String,
30 pub(crate) mime_type: String,
31 pub(crate) url: Option<Url>,
32}
33
34impl Default for ResourceTemplate {
35 fn default() -> Self {
36 Self {
37 description: String::new(),
38 mime_type: "application/json".to_owned(),
39 url: None,
40 }
41 }
42}
43
44impl ResourceTemplate {
45 pub(crate) fn resolve(&self, base_url: &Url, req: &Request) -> ResourceInfo {
49 let url = self.url.as_ref().map_or_else(
50 || {
51 let mut url = base_url.clone();
52 url.set_path(req.uri().path());
53 url.set_query(req.uri().query());
54 url.to_string()
55 },
56 ToString::to_string,
57 );
58 let mut info = ResourceInfo::new(url);
59 if !self.description.is_empty() {
60 info = info.with_description(self.description.clone());
61 }
62 if !self.mime_type.is_empty() {
63 info = info.with_mime_type(self.mime_type.clone());
64 }
65 info
66 }
67}
68
69#[derive(Clone)]
71pub struct X402Layer<TSource> {
72 pub(crate) server: ResourceServer,
73 pub(crate) base_url: Arc<Url>,
74 pub(crate) price_source: TSource,
75 pub(crate) resource: Arc<ResourceTemplate>,
76 pub(crate) settlement_mode: SettlementMode,
77 pub(crate) settlement_tracker: Option<BackgroundSettlementTracker>,
78 pub(crate) hooks: Option<Arc<dyn DynGateHooks>>,
79 #[cfg(feature = "siwx")]
80 pub(crate) siwx: Option<Arc<super::SiwxGate>>,
81}
82
83impl X402Layer<StaticPriceTags> {
84 pub fn with_price_tag(mut self, price_tag: PriceTag) -> Result<Self, BuildError> {
91 self.price_source = self.price_source.with_price_tag(price_tag);
92 self.validate_static()?;
93 Ok(self)
94 }
95
96 pub fn with_settlement_mode(mut self, mode: SettlementMode) -> Result<Self, BuildError> {
108 self.settlement_mode = mode;
109 self.validate_static()?;
110 Ok(self)
111 }
112
113 #[cfg(feature = "siwx")]
120 pub fn with_siwx(mut self, gate: super::SiwxGate) -> Result<Self, BuildError> {
121 self.siwx = Some(Arc::new(gate));
122 self.validate_static()?;
123 Ok(self)
124 }
125
126 #[cfg(feature = "siwx")]
132 pub fn with_auth_only(self, gate: super::SiwxGate) -> Result<Self, BuildError> {
133 self.with_siwx(gate.with_auth_only())
134 }
135
136 fn validate_static(&self) -> Result<(), BuildError> {
137 validate_static_layer(
138 &self.server,
139 self.price_source.tags(),
140 self.settlement_mode,
141 self.static_auth_only(),
142 )
143 }
144
145 fn static_auth_only(&self) -> bool {
146 #[cfg(feature = "siwx")]
147 {
148 self.siwx.as_ref().is_some_and(|g| g.is_auth_only())
149 }
150 #[cfg(not(feature = "siwx"))]
151 {
152 let _ = self;
153 false
154 }
155 }
156}
157
158impl X402Layer<DynamicPriceTags> {
159 #[allow(
169 clippy::unnecessary_wraps,
170 clippy::missing_const_for_fn,
171 reason = "same Result signature as Static"
172 )]
173 pub fn with_settlement_mode(mut self, mode: SettlementMode) -> Result<Self, BuildError> {
174 self.settlement_mode = mode;
175 Ok(self)
176 }
177
178 #[cfg(feature = "siwx")]
180 #[must_use]
181 pub fn with_siwx(mut self, gate: super::SiwxGate) -> Self {
182 self.siwx = Some(Arc::new(gate));
183 self
184 }
185
186 #[cfg(feature = "siwx")]
188 #[must_use]
189 pub fn with_auth_only(self, gate: super::SiwxGate) -> Self {
190 self.with_siwx(gate.with_auth_only())
191 }
192}
193
194impl<TSource: std::fmt::Debug> std::fmt::Debug for X402Layer<TSource> {
195 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
196 f.debug_struct("X402Layer")
197 .field("base_url", &self.base_url)
198 .field("price_source", &self.price_source)
199 .field("settlement_mode", &self.settlement_mode)
200 .finish_non_exhaustive()
201 }
202}
203
204impl<TSource> X402Layer<TSource> {
205 #[must_use]
207 pub fn with_scheme(
208 mut self,
209 network: ChainIdPattern,
210 scheme: impl SchemeNetworkServer + 'static,
211 ) -> Self {
212 self.server.register_scheme(network, scheme);
213 self
214 }
215
216 #[must_use]
218 pub fn with_extension(mut self, extension: impl Extension + 'static) -> Self {
219 self.server = self.server.with_extension(extension);
220 self
221 }
222
223 #[must_use]
225 pub fn with_description(mut self, description: String) -> Self {
226 let mut new_resource = (*self.resource).clone();
227 new_resource.description = description;
228 self.resource = Arc::new(new_resource);
229 self
230 }
231
232 #[must_use]
234 pub fn with_mime_type(mut self, mime: String) -> Self {
235 let mut new_resource = (*self.resource).clone();
236 new_resource.mime_type = mime;
237 self.resource = Arc::new(new_resource);
238 self
239 }
240
241 #[must_use]
243 pub fn with_resource(mut self, resource: Url) -> Self {
244 let mut new_resource = (*self.resource).clone();
245 new_resource.url = Some(resource);
246 self.resource = Arc::new(new_resource);
247 self
248 }
249
250 #[must_use]
252 pub fn with_settlement_tracker(mut self, tracker: BackgroundSettlementTracker) -> Self {
253 self.settlement_tracker = Some(tracker);
254 self
255 }
256
257 #[must_use]
259 pub fn with_hooks<H>(mut self, hooks: H) -> Self
260 where
261 H: GateHooks + 'static,
262 {
263 self.hooks = Some(Arc::new(hooks));
264 self
265 }
266}
267
268impl<S, TSource> Layer<S> for X402Layer<TSource>
269where
270 S: Service<Request, Response = Response, Error = Infallible> + Clone + Send + Sync + 'static,
271 S::Future: Send + 'static,
272 TSource: PriceTagSource,
273{
274 type Service = X402MiddlewareService<TSource>;
275
276 fn layer(&self, inner: S) -> Self::Service {
277 X402MiddlewareService {
278 server: self.server.clone(),
279 base_url: Arc::clone(&self.base_url),
280 price_source: self.price_source.clone(),
281 resource: Arc::clone(&self.resource),
282 settlement_mode: self.settlement_mode,
283 settlement_tracker: self.settlement_tracker.clone(),
284 hooks: self.hooks.clone(),
285 #[cfg(feature = "siwx")]
286 siwx: self.siwx.clone(),
287 inner: BoxCloneSyncService::new(inner),
288 }
289 }
290}
291
292#[derive(Clone)]
294#[allow(
295 missing_debug_implementations,
296 reason = "BoxCloneSyncService does not impl Debug"
297)]
298pub struct X402MiddlewareService<TSource> {
299 server: ResourceServer,
300 base_url: Arc<Url>,
301 price_source: TSource,
302 resource: Arc<ResourceTemplate>,
303 settlement_mode: SettlementMode,
304 settlement_tracker: Option<BackgroundSettlementTracker>,
305 hooks: Option<Arc<dyn DynGateHooks>>,
306 #[cfg(feature = "siwx")]
307 siwx: Option<Arc<super::SiwxGate>>,
308 inner: BoxCloneSyncService<Request, Response, Infallible>,
309}
310
311impl<TSource> Service<Request> for X402MiddlewareService<TSource>
312where
313 TSource: PriceTagSource,
314{
315 type Response = Response;
316 type Error = Infallible;
317 type Future = Pin<Box<dyn Future<Output = Result<Response, Infallible>> + Send>>;
318
319 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
320 self.inner.poll_ready(cx)
321 }
322
323 fn call(&mut self, req: Request) -> Self::Future {
324 Box::pin(enforce(
325 self.price_source.clone(),
326 self.server.clone(),
327 Arc::clone(&self.base_url),
328 Arc::clone(&self.resource),
329 self.settlement_mode,
330 self.settlement_tracker.clone(),
331 self.hooks.clone(),
332 #[cfg(feature = "siwx")]
333 self.siwx.clone(),
334 self.inner.clone(),
335 req,
336 ))
337 }
338}
339
340#[allow(
341 clippy::too_many_arguments,
342 clippy::excessive_nesting,
343 reason = "cloned service fields; hook match must borrow Request only for the await"
344)]
345async fn enforce<TSource: PriceTagSource>(
346 price_source: TSource,
347 server: ResourceServer,
348 base_url: Arc<Url>,
349 resource_builder: Arc<ResourceTemplate>,
350 settlement_mode: SettlementMode,
351 settlement_tracker: Option<BackgroundSettlementTracker>,
352 hooks: Option<Arc<dyn DynGateHooks>>,
353 #[cfg(feature = "siwx")] siwx: Option<Arc<super::SiwxGate>>,
354 mut inner: BoxCloneSyncService<Request, Response, Infallible>,
355 req: Request,
356) -> Result<Response, Infallible> {
357 #[cfg(feature = "siwx")]
358 if let Some(siwx_gate) = siwx.as_ref() {
359 let header = req
360 .headers()
361 .get(crate::headers::SIGN_IN_WITH_X)
362 .and_then(|v| v.to_str().ok())
363 .map(str::to_owned);
364 let path = req.uri().path().to_owned();
365 if let Some(header) = header
366 && siwx_gate.try_grant(&header, &path).await
367 {
368 return inner.call(req).await;
369 }
370 }
371
372 if let Some(h) = hooks.as_ref() {
373 match h.on_protected_request(&req).await {
374 ProtectedRequestOutcome::Continue => {}
375 ProtectedRequestOutcome::GrantAccess => return inner.call(req).await,
376 ProtectedRequestOutcome::Abort { status, body } => {
377 return Ok(abort_response(status, body));
378 }
379 }
380 }
381
382 let accepts = price_source
383 .resolve(req.headers(), req.uri(), base_url.as_ref())
384 .await;
385 #[cfg(feature = "siwx")]
386 let auth_only = siwx.as_ref().is_some_and(|g| g.is_auth_only());
387 #[cfg(not(feature = "siwx"))]
388 let auth_only = false;
389 if accepts.is_empty() && !auth_only {
390 return inner.call(req).await;
391 }
392
393 let resource = resource_builder.resolve(base_url.as_ref(), &req);
394 let mut gate = Gate::from_parts(
395 server,
396 accepts,
397 resource,
398 settlement_mode,
399 settlement_tracker,
400 hooks,
401 #[cfg(feature = "siwx")]
402 siwx.clone(),
403 );
404 if let Err(err) = gate.require_schemes().await {
405 return Ok(gate.error_response(err));
406 }
407 if let Err(err) = gate.assert_mode_compatible(settlement_mode) {
408 return Ok(gate.error_response(err));
409 }
410 if let Err(err) = gate.build_payment_required().await {
411 return Ok(gate.error_response(err));
412 }
413 #[cfg(feature = "siwx")]
414 if let Some(siwx) = siwx.as_ref()
415 && let Err(err) = gate.attach_siwx_challenge(siwx, req.uri().path())
416 {
417 return Ok(gate.error_response(err));
418 }
419
420 let result = match settlement_mode {
421 SettlementMode::Sequential => gate.handle_request(inner, req).await,
422 SettlementMode::Concurrent => {
423 Box::pin(gate.handle_request_concurrent(inner, req)).await
425 }
426 SettlementMode::Background => gate.handle_request_background(inner, req).await,
427 };
428 Ok(result.unwrap_or_else(|err| gate.error_response(err)))
429}