1use crate::middleware::PaymentMiddlewareConfig;
7use crate::types::{FacilitatorConfig, Network};
8use crate::{Result, X402Error};
9use axum::{
10 extract::State,
11 http::{HeaderMap, HeaderName, HeaderValue, Method, StatusCode},
12 response::{IntoResponse, Response},
13 routing::any,
14 Router,
15};
16use rust_decimal::Decimal;
17use serde::{Deserialize, Serialize};
18use std::collections::HashMap;
19use std::str::FromStr;
20use tower::ServiceBuilder;
21use tower_http::trace::TraceLayer;
22use tracing::{info, warn};
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct ProxyConfig {
27 pub target_url: String,
29 pub amount: f64,
31 pub pay_to: String,
33 pub description: Option<String>,
35 pub mime_type: Option<String>,
37 pub max_timeout_seconds: u32,
39 pub facilitator_url: String,
41 pub testnet: bool,
43 pub headers: HashMap<String, String>,
45 pub cdp_api_key_id: Option<String>,
47 pub cdp_api_key_secret: Option<String>,
48}
49
50impl Default for ProxyConfig {
51 fn default() -> Self {
52 Self {
53 target_url: String::new(),
54 amount: 0.0001,
55 pay_to: String::new(),
56 description: None,
57 mime_type: None,
58 max_timeout_seconds: 60,
59 facilitator_url: "https://x402.org/facilitator".to_string(),
60 testnet: true,
61 headers: HashMap::new(),
62 cdp_api_key_id: None,
63 cdp_api_key_secret: None,
64 }
65 }
66}
67
68impl ProxyConfig {
69 pub fn from_file(path: &str) -> Result<Self> {
71 let content = std::fs::read_to_string(path)
72 .map_err(|e| X402Error::config(format!("Failed to read config file: {}", e)))?;
73
74 let config: ProxyConfig = serde_json::from_str(&content)
75 .map_err(|e| X402Error::config(format!("Failed to parse config file: {}", e)))?;
76
77 config.validate()?;
78 Ok(config)
79 }
80
81 pub fn from_env() -> Result<Self> {
83 let mut config = Self::default();
84
85 if let Ok(target_url) = std::env::var("TARGET_URL") {
86 config.target_url = target_url;
87 }
88
89 if let Ok(amount) = std::env::var("AMOUNT") {
90 config.amount = amount
91 .parse()
92 .map_err(|e| X402Error::config(format!("Invalid AMOUNT: {}", e)))?;
93 }
94
95 if let Ok(pay_to) = std::env::var("PAY_TO") {
96 config.pay_to = pay_to;
97 }
98
99 if let Ok(description) = std::env::var("DESCRIPTION") {
100 config.description = Some(description);
101 }
102
103 if let Ok(facilitator_url) = std::env::var("FACILITATOR_URL") {
104 config.facilitator_url = facilitator_url;
105 }
106
107 if let Ok(testnet) = std::env::var("TESTNET") {
108 config.testnet = testnet
109 .parse()
110 .map_err(|e| X402Error::config(format!("Invalid TESTNET: {}", e)))?;
111 }
112
113 if let Ok(cdp_api_key_id) = std::env::var("CDP_API_KEY_ID") {
114 config.cdp_api_key_id = Some(cdp_api_key_id);
115 }
116
117 if let Ok(cdp_api_key_secret) = std::env::var("CDP_API_KEY_SECRET") {
118 config.cdp_api_key_secret = Some(cdp_api_key_secret);
119 }
120
121 config.validate()?;
122 Ok(config)
123 }
124
125 pub fn validate(&self) -> Result<()> {
127 if self.target_url.is_empty() {
128 return Err(X402Error::config("TARGET_URL is required"));
129 }
130
131 if self.pay_to.is_empty() {
132 return Err(X402Error::config("PAY_TO is required"));
133 }
134
135 if self.amount <= 0.0 {
136 return Err(X402Error::config("AMOUNT must be positive"));
137 }
138
139 url::Url::parse(&self.target_url)
141 .map_err(|e| X402Error::config(format!("Invalid TARGET_URL: {}", e)))?;
142
143 url::Url::parse(&self.facilitator_url)
145 .map_err(|e| X402Error::config(format!("Invalid FACILITATOR_URL: {}", e)))?;
146
147 Ok(())
148 }
149
150 pub fn to_payment_config(&self) -> Result<PaymentMiddlewareConfig> {
152 let amount = Decimal::from_str(&self.amount.to_string())
153 .map_err(|e| X402Error::config(format!("Invalid amount: {}", e)))?;
154
155 let mut facilitator_config = FacilitatorConfig::new(&self.facilitator_url);
156
157 if let (Some(api_key_id), Some(api_key_secret)) =
159 (&self.cdp_api_key_id, &self.cdp_api_key_secret)
160 {
161 if !api_key_id.is_empty() && !api_key_secret.is_empty() {
162 let auth_headers =
163 crate::facilitator::coinbase::create_auth_headers(api_key_id, api_key_secret);
164 facilitator_config = facilitator_config.with_auth_headers(Box::new(auth_headers));
165 }
166 }
167
168 let _network = if self.testnet {
169 Network::Testnet
170 } else {
171 Network::Mainnet
172 };
173
174 let mut config = PaymentMiddlewareConfig::new(amount, &self.pay_to)
175 .with_facilitator_config(facilitator_config)
176 .with_testnet(self.testnet)
177 .with_max_timeout_seconds(self.max_timeout_seconds);
178
179 if let Some(description) = &self.description {
180 config = config.with_description(description);
181 }
182
183 if let Some(mime_type) = &self.mime_type {
184 config = config.with_mime_type(mime_type);
185 }
186
187 Ok(config)
188 }
189}
190
191#[derive(Clone)]
193pub struct ProxyState {
194 config: ProxyConfig,
195 client: reqwest::Client,
196}
197
198impl ProxyState {
199 pub fn new(config: ProxyConfig) -> Result<Self> {
200 let client = reqwest::Client::builder()
201 .timeout(std::time::Duration::from_secs(30))
202 .build()
203 .map_err(|e| X402Error::config(format!("Failed to create HTTP client: {}", e)))?;
204
205 Ok(Self { config, client })
206 }
207}
208
209pub fn create_proxy_server(config: ProxyConfig) -> Result<Router> {
211 let state = ProxyState::new(config.clone())?;
212
213 let app = Router::new()
214 .route("/*path", any(proxy_handler))
215 .with_state(state);
216
217 Ok(app)
218}
219
220pub fn create_proxy_server_with_tracing(config: ProxyConfig) -> Result<Router> {
222 let state = ProxyState::new(config.clone())?;
223
224 let app = Router::new()
225 .route("/*path", any(proxy_handler))
226 .with_state(state)
227 .layer(ServiceBuilder::new().layer(TraceLayer::new_for_http()));
228
229 Ok(app)
230}
231
232pub fn create_proxy_server_with_payment(config: ProxyConfig) -> Result<Router> {
234 let state = ProxyState::new(config.clone())?;
235
236 let payment_config = config.to_payment_config()?;
238 let payment_middleware = crate::middleware::PaymentMiddleware::new(
239 payment_config.amount,
240 payment_config.pay_to.clone(),
241 )
242 .with_facilitator_config(payment_config.facilitator_config.clone())
243 .with_testnet(payment_config.testnet)
244 .with_description(
245 payment_config
246 .description
247 .as_deref()
248 .unwrap_or("Proxy payment"),
249 );
250
251 let app = Router::new()
252 .route("/*path", any(proxy_handler_with_payment))
253 .with_state(state)
254 .layer(axum::middleware::from_fn_with_state(
255 payment_middleware,
256 payment_middleware_handler,
257 ));
258
259 Ok(app)
260}
261
262async fn payment_middleware_handler(
264 State(middleware): State<crate::middleware::PaymentMiddleware>,
265 request: axum::extract::Request,
266 next: axum::middleware::Next,
267) -> impl axum::response::IntoResponse {
268 match middleware.process_payment(request, next).await {
269 Ok(result) => match result {
270 crate::middleware::PaymentResult::Success { response, .. } => response,
271 crate::middleware::PaymentResult::PaymentRequired { response } => response,
272 crate::middleware::PaymentResult::VerificationFailed { response } => response,
273 crate::middleware::PaymentResult::SettlementFailed { response } => response,
274 },
275 Err(e) => (
276 axum::http::StatusCode::INTERNAL_SERVER_ERROR,
277 axum::Json(serde_json::json!({
278 "error": format!("Payment processing error: {}", e),
279 "x402Version": 1
280 })),
281 )
282 .into_response(),
283 }
284}
285
286async fn proxy_handler_with_payment(
288 State(state): State<ProxyState>,
289 request: axum::extract::Request,
290) -> std::result::Result<Response, StatusCode> {
291 proxy_handler(State(state), request).await
293}
294
295async fn proxy_handler(
297 State(state): State<ProxyState>,
298 request: axum::extract::Request,
299) -> std::result::Result<Response, StatusCode> {
300 let target_url = &state.config.target_url;
301 let client = &state.client;
302
303 let path = request.uri().path();
305 let query = request.uri().query().unwrap_or("");
306
307 let full_url = if query.is_empty() {
309 format!("{}{}", target_url, path)
310 } else {
311 format!("{}{}?{}", target_url, path, query)
312 };
313
314 info!("Proxying request to: {}", full_url);
315
316 let method =
318 Method::from_str(request.method().as_str()).map_err(|_| StatusCode::BAD_REQUEST)?;
319
320 let mut target_request = client.request(method, &full_url);
321
322 target_request = copy_essential_headers(request.headers(), target_request);
324
325 for (key, value) in &state.config.headers {
327 if let (Ok(name), Ok(val)) = (HeaderName::try_from(key), HeaderValue::try_from(value)) {
328 target_request = target_request.header(name, val);
329 }
330 }
331
332 let body = axum::body::to_bytes(request.into_body(), usize::MAX)
334 .await
335 .map_err(|_| StatusCode::BAD_REQUEST)?;
336
337 if !body.is_empty() {
338 target_request = target_request.body(body);
339 }
340
341 let response = target_request.send().await.map_err(|e| {
343 warn!("Failed to execute proxy request: {}", e);
344 StatusCode::BAD_GATEWAY
345 })?;
346
347 let status = response.status();
349 let headers = response.headers().clone();
350 let body = response
351 .bytes()
352 .await
353 .map_err(|_| StatusCode::BAD_GATEWAY)?;
354
355 let mut response_builder = Response::builder().status(status);
356
357 for (key, value) in headers.iter() {
359 if let Ok(header_name) = HeaderName::try_from(key.as_str()) {
360 response_builder = response_builder.header(header_name, value);
361 }
362 }
363
364 response_builder
365 .body(body.into())
366 .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)
367}
368
369fn copy_essential_headers(
371 source_headers: &HeaderMap,
372 target_request: reqwest::RequestBuilder,
373) -> reqwest::RequestBuilder {
374 let essential_headers = [
375 "user-agent",
376 "accept",
377 "accept-language",
378 "accept-encoding",
379 "content-type",
380 "content-length",
381 "authorization",
382 "x-requested-with",
383 ];
384
385 let mut request = target_request;
386
387 for header_name in &essential_headers {
388 if let Some(value) = source_headers.get(*header_name) {
389 if let Ok(name) = HeaderName::try_from(*header_name) {
390 request = request.header(name, value);
391 }
392 }
393 }
394
395 request
396}
397
398pub async fn run_proxy_server(config: ProxyConfig, port: u16) -> Result<()> {
400 let app = create_proxy_server_with_tracing(config)?;
401
402 let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{}", port))
403 .await
404 .map_err(|e| X402Error::config(format!("Failed to bind to port {}: {}", port, e)))?;
405
406 info!("🚀 Proxy server running on port {}", port);
407 info!("💰 All requests will require payment");
408
409 axum::serve(listener, app)
410 .await
411 .map_err(|e| X402Error::config(format!("Server error: {}", e)))?;
412
413 Ok(())
414}
415
416#[cfg(test)]
417mod tests {
418 use super::*;
419
420 #[test]
421 fn test_proxy_config_default() {
422 let config = ProxyConfig::default();
423 assert_eq!(config.amount, 0.0001);
424 assert!(config.testnet);
425 assert_eq!(config.facilitator_url, "https://x402.org/facilitator");
426 }
427
428 #[test]
429 fn test_proxy_config_validation() {
430 let config = ProxyConfig {
431 target_url: "https://example.com".to_string(),
432 pay_to: "0x1234567890123456789012345678901234567890".to_string(),
433 ..Default::default()
434 };
435
436 let result = config.validate();
437 assert!(result.is_ok(), "Valid config should pass validation");
438
439 assert_eq!(config.target_url, "https://example.com");
441 assert_eq!(config.pay_to, "0x1234567890123456789012345678901234567890");
442 assert!(config.testnet, "Default should be testnet");
443 }
444
445 #[test]
446 fn test_proxy_config_validation_missing_target() {
447 let config = ProxyConfig::default();
448 let result = config.validate();
449 assert!(
450 result.is_err(),
451 "Config without target URL should fail validation"
452 );
453
454 let error_msg = result.unwrap_err().to_string();
456 assert!(
457 error_msg.contains("TARGET_URL is required"),
458 "Error should mention TARGET_URL is required - actual: {}",
459 error_msg
460 );
461 }
462
463 #[test]
464 fn test_proxy_config_validation_invalid_url() {
465 let config = ProxyConfig {
466 target_url: "not-a-url".to_string(),
467 pay_to: "0x1234567890123456789012345678901234567890".to_string(),
468 ..Default::default()
469 };
470
471 let result = config.validate();
472 assert!(
473 result.is_err(),
474 "Config with invalid URL should fail validation"
475 );
476
477 let error_msg = result.unwrap_err().to_string();
479 assert!(
480 error_msg.contains("invalid URL") || error_msg.contains("URL"),
481 "Error should mention invalid URL - actual: {}",
482 error_msg
483 );
484 }
485
486 #[test]
487 fn test_proxy_config_to_payment_config() {
488 let config = ProxyConfig {
489 target_url: "https://example.com".to_string(),
490 pay_to: "0x1234567890123456789012345678901234567890".to_string(),
491 amount: 0.01,
492 description: Some("Test payment".to_string()),
493 ..Default::default()
494 };
495
496 let payment_config = config.to_payment_config().unwrap();
497 assert_eq!(
498 payment_config.pay_to,
499 "0x1234567890123456789012345678901234567890"
500 );
501 assert!(payment_config.testnet);
502 }
503}