auth_framework/server/core/
common_http.rs

1//! Common HTTP Client Utilities
2//!
3//! This module provides shared HTTP client functionality to eliminate
4//! duplication across server modules.
5
6use crate::errors::{AuthError, Result};
7use crate::server::core::common_config::{EndpointConfig, RetryConfig};
8use reqwest::{Client, Method, RequestBuilder, Response};
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11use std::time::Duration;
12use tokio::time::{sleep, timeout};
13
14/// HTTP client wrapper with common functionality
15#[derive(Clone, Debug)]
16pub struct HttpClient {
17    client: Client,
18    config: EndpointConfig,
19    retry_config: RetryConfig,
20}
21
22impl HttpClient {
23    /// Create new HTTP client
24    pub fn new(config: EndpointConfig) -> Result<Self> {
25        let mut client_builder = Client::builder()
26            .timeout(Duration::from_secs(
27                config.timeout.connect_timeout.as_secs(),
28            ))
29            .connect_timeout(config.timeout.connect_timeout)
30            .danger_accept_invalid_certs(!config.security.enable_tls);
31
32        // Add default headers
33        let mut headers = reqwest::header::HeaderMap::new();
34        for (key, value) in &config.headers {
35            let header_name =
36                reqwest::header::HeaderName::from_bytes(key.as_bytes()).map_err(|e| {
37                    AuthError::ConfigurationError(format!("Invalid header name: {}", e))
38                })?;
39            let header_value = reqwest::header::HeaderValue::from_str(value).map_err(|e| {
40                AuthError::ConfigurationError(format!("Invalid header value: {}", e))
41            })?;
42            headers.insert(header_name, header_value);
43        }
44
45        if !headers.contains_key("user-agent") {
46            headers.insert(
47                reqwest::header::USER_AGENT,
48                reqwest::header::HeaderValue::from_static("auth-framework/0.3.0"),
49            );
50        }
51
52        client_builder = client_builder.default_headers(headers);
53
54        let client = client_builder.build().map_err(|e| {
55            AuthError::ConfigurationError(format!("Failed to create HTTP client: {}", e))
56        })?;
57
58        Ok(Self {
59            client,
60            config,
61            retry_config: RetryConfig::default(),
62        })
63    }
64
65    /// Set retry configuration
66    pub fn with_retry_config(mut self, retry_config: RetryConfig) -> Self {
67        self.retry_config = retry_config;
68        self
69    }
70
71    /// Execute GET request with retries
72    pub async fn get(&self, path: &str) -> Result<Response> {
73        let url = self.build_url(path)?;
74        self.execute_with_retry(Method::GET, &url, None::<&()>)
75            .await
76    }
77
78    /// Create POST request builder (reqwest-compatible)
79    pub fn post(&self, url: &str) -> RequestBuilder {
80        self.client.post(url)
81    }
82
83    /// Execute POST request with JSON body
84    pub async fn post_json<T>(&self, path: &str, body: &T) -> Result<Response>
85    where
86        T: Serialize,
87    {
88        let url = self.build_url(path)?;
89        self.execute_with_retry(Method::POST, &url, Some(body))
90            .await
91    }
92
93    /// Execute PUT request with JSON body
94    pub async fn put_json<T>(&self, path: &str, body: &T) -> Result<Response>
95    where
96        T: Serialize,
97    {
98        let url = self.build_url(path)?;
99        self.execute_with_retry(Method::PUT, &url, Some(body)).await
100    }
101
102    /// Execute DELETE request
103    pub async fn delete(&self, path: &str) -> Result<Response> {
104        let url = self.build_url(path)?;
105        self.execute_with_retry(Method::DELETE, &url, None::<&()>)
106            .await
107    }
108
109    /// Execute form-encoded POST request
110    pub async fn post_form(
111        &self,
112        path: &str,
113        form_data: &HashMap<String, String>,
114    ) -> Result<Response> {
115        let url = self.build_url(path)?;
116
117        let mut request = self.client.request(Method::POST, &url);
118        request = request.form(form_data);
119
120        self.execute_request_with_retry(request).await
121    }
122
123    /// Execute request with custom headers
124    pub async fn request_with_headers<T>(
125        &self,
126        method: Method,
127        path: &str,
128        headers: HashMap<String, String>,
129        body: Option<&T>,
130    ) -> Result<Response>
131    where
132        T: Serialize,
133    {
134        let url = self.build_url(path)?;
135        let mut request = self.client.request(method, &url);
136
137        // Add custom headers
138        for (key, value) in headers {
139            request = request.header(key, value);
140        }
141
142        // Add body if provided
143        if let Some(body) = body {
144            request = request.json(body);
145        }
146
147        self.execute_request_with_retry(request).await
148    }
149
150    /// Build full URL from base and path
151    fn build_url(&self, path: &str) -> Result<String> {
152        let mut url = self.config.base_url.clone();
153
154        // Add API version if configured
155        if let Some(ref version) = self.config.api_version {
156            if !url.ends_with('/') {
157                url.push('/');
158            }
159            url.push_str(version);
160        }
161
162        // Add path
163        if !url.ends_with('/') && !path.starts_with('/') {
164            url.push('/');
165        }
166        url.push_str(path);
167
168        Ok(url)
169    }
170
171    /// Execute request with retry logic
172    async fn execute_with_retry<T>(
173        &self,
174        method: Method,
175        url: &str,
176        body: Option<&T>,
177    ) -> Result<Response>
178    where
179        T: Serialize,
180    {
181        let mut request = self.client.request(method, url);
182
183        if let Some(body) = body {
184            request = request.json(body);
185        }
186
187        self.execute_request_with_retry(request).await
188    }
189
190    /// Execute request with retry logic
191    async fn execute_request_with_retry(
192        &self,
193        request_builder: RequestBuilder,
194    ) -> Result<Response> {
195        let mut last_error = None;
196
197        for attempt in 0..=self.retry_config.max_attempts {
198            let request = request_builder
199                .try_clone()
200                .ok_or_else(|| AuthError::validation("Cannot clone request for retry"))?;
201
202            match timeout(self.config.timeout.read_timeout, request.send()).await {
203                Ok(Ok(response)) => {
204                    if response.status().is_success() || !self.is_retryable_error(&response) {
205                        return Ok(response);
206                    }
207                    last_error = Some(AuthError::validation(format!("HTTP {}", response.status())));
208                }
209                Ok(Err(e)) => {
210                    last_error = Some(AuthError::validation(format!("Request failed: {}", e)));
211                }
212                Err(_) => {
213                    last_error = Some(AuthError::validation("Request timeout"));
214                }
215            }
216
217            // Don't sleep after the last attempt
218            if attempt < self.retry_config.max_attempts {
219                let delay = self.calculate_retry_delay(attempt);
220                sleep(delay).await;
221            }
222        }
223
224        Err(last_error.unwrap_or_else(|| AuthError::validation("All retry attempts failed")))
225    }
226
227    /// Check if error is retryable
228    fn is_retryable_error(&self, response: &Response) -> bool {
229        match response.status().as_u16() {
230            // Retry on server errors and some client errors
231            500..=599 => true, // Server errors
232            429 => true,       // Rate limiting
233            408 => true,       // Request timeout
234            _ => false,
235        }
236    }
237
238    /// Calculate retry delay with exponential backoff and jitter
239    fn calculate_retry_delay(&self, attempt: u32) -> Duration {
240        let base_delay = self.retry_config.initial_delay.as_millis() as f64;
241        let backoff = self.retry_config.backoff_multiplier.powi(attempt as i32);
242        let delay_ms = (base_delay * backoff).min(self.retry_config.max_delay.as_millis() as f64);
243
244        // Add jitter
245        let jitter = delay_ms * self.retry_config.jitter_factor * (rand::random::<f64>() - 0.5);
246        let final_delay = (delay_ms + jitter).max(0.0) as u64;
247
248        Duration::from_millis(final_delay)
249    }
250}
251
252/// Common HTTP response handling utilities
253pub mod response {
254    use super::*;
255
256    /// Parse JSON response with error handling
257    pub async fn parse_json<T>(response: Response) -> Result<T>
258    where
259        T: for<'de> Deserialize<'de>,
260    {
261        if !response.status().is_success() {
262            let status = response.status();
263            let body = response
264                .text()
265                .await
266                .unwrap_or_else(|_| "Failed to read error response body".to_string());
267
268            return Err(AuthError::validation(format!("HTTP {} - {}", status, body)));
269        }
270
271        response
272            .json::<T>()
273            .await
274            .map_err(|e| AuthError::validation(format!("Failed to parse JSON response: {}", e)))
275    }
276
277    /// Extract response body as text
278    pub async fn extract_text(response: Response) -> Result<String> {
279        if !response.status().is_success() {
280            let status = response.status();
281            let body = response
282                .text()
283                .await
284                .unwrap_or_else(|_| "Failed to read error response body".to_string());
285
286            return Err(AuthError::validation(format!("HTTP {} - {}", status, body)));
287        }
288
289        response
290            .text()
291            .await
292            .map_err(|e| AuthError::validation(format!("Failed to read response body: {}", e)))
293    }
294
295    /// Check if response indicates success
296    pub fn is_success_status(status_code: u16) -> bool {
297        (200..300).contains(&status_code)
298    }
299
300    /// Extract error details from response
301    pub async fn extract_error_details(response: Response) -> (u16, String) {
302        let status = response.status().as_u16();
303        let body = response
304            .text()
305            .await
306            .unwrap_or_else(|_| "Unable to read response body".to_string());
307        (status, body)
308    }
309}
310
311/// OAuth-specific HTTP client utilities
312pub mod oauth {
313    use super::*;
314
315    /// Execute OAuth token exchange request
316    pub async fn token_exchange(
317        client: &HttpClient,
318        token_endpoint: &str,
319        params: &HashMap<String, String>,
320    ) -> Result<serde_json::Value> {
321        // Use relative path from base_url or full URL
322        let path = if token_endpoint.starts_with("http") {
323            // Override base_url for this request
324            return execute_absolute_url_form_post(client, token_endpoint, params).await;
325        } else {
326            token_endpoint
327        };
328
329        let response = client.post_form(path, params).await?;
330        response::parse_json(response).await
331    }
332
333    /// Execute introspection request
334    pub async fn introspect_token(
335        client: &HttpClient,
336        introspect_endpoint: &str,
337        token: &str,
338        client_id: Option<&str>,
339    ) -> Result<serde_json::Value> {
340        let mut params = HashMap::new();
341        params.insert("token".to_string(), token.to_string());
342
343        if let Some(client_id) = client_id {
344            params.insert("client_id".to_string(), client_id.to_string());
345        }
346
347        let response = client.post_form(introspect_endpoint, &params).await?;
348        response::parse_json(response).await
349    }
350
351    /// Execute JWKS fetch
352    pub async fn fetch_jwks(client: &HttpClient, jwks_uri: &str) -> Result<serde_json::Value> {
353        let response = client.get(jwks_uri).await?;
354        response::parse_json(response).await
355    }
356
357    /// Execute OAuth discovery request
358    pub async fn discover_configuration(
359        _client: &HttpClient,
360        issuer: &str,
361    ) -> Result<serde_json::Value> {
362        let discovery_url = format!(
363            "{}/.well-known/openid_configuration",
364            issuer.trim_end_matches('/')
365        );
366
367        // Create temporary client for absolute URL
368        let temp_config = EndpointConfig::new(&discovery_url);
369        let temp_client = HttpClient::new(temp_config)?;
370
371        let response = temp_client.get("").await?;
372        response::parse_json(response).await
373    }
374
375    /// Execute form POST to absolute URL
376    async fn execute_absolute_url_form_post(
377        _client: &HttpClient,
378        url: &str,
379        params: &HashMap<String, String>,
380    ) -> Result<serde_json::Value> {
381        // Create client for specific URL
382        let temp_config = EndpointConfig::new(url);
383        let temp_client = HttpClient::new(temp_config)?;
384
385        let response = temp_client.post_form("", params).await?;
386        response::parse_json(response).await
387    }
388}
389
390/// Webhook and callback utilities
391pub mod webhooks {
392    use super::*;
393
394    /// Send webhook notification
395    pub async fn send_webhook<T>(
396        client: &HttpClient,
397        webhook_url: &str,
398        payload: &T,
399        signature_key: Option<&str>,
400    ) -> Result<()>
401    where
402        T: Serialize,
403    {
404        let mut headers = HashMap::new();
405        headers.insert("Content-Type".to_string(), "application/json".to_string());
406
407        // Add signature if key provided
408        if let Some(key) = signature_key {
409            let payload_json = serde_json::to_string(payload).map_err(|e| {
410                AuthError::validation(format!("Failed to serialize payload: {}", e))
411            })?;
412            let signature = calculate_webhook_signature(&payload_json, key)?;
413            headers.insert("X-Webhook-Signature".to_string(), signature);
414        }
415
416        let response = client
417            .request_with_headers(Method::POST, webhook_url, headers, Some(payload))
418            .await?;
419
420        if !response.status().is_success() {
421            return Err(AuthError::validation(format!(
422                "Webhook failed: {}",
423                response.status()
424            )));
425        }
426
427        Ok(())
428    }
429
430    /// Calculate HMAC signature for webhook
431    fn calculate_webhook_signature(payload: &str, key: &str) -> Result<String> {
432        // Simplified signature calculation without external HMAC dependency
433        // In a real implementation, you'd use the `hmac` crate
434        use std::collections::hash_map::DefaultHasher;
435        use std::hash::{Hash, Hasher};
436
437        let mut hasher = DefaultHasher::new();
438        key.hash(&mut hasher);
439        payload.hash(&mut hasher);
440        let hash_result = hasher.finish();
441
442        Ok(format!("sha256={:x}", hash_result))
443    }
444}
445
446