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