auth_framework/server/core/
common_http.rs1use 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#[derive(Clone, Debug)]
16pub struct HttpClient {
17 client: Client,
18 config: EndpointConfig,
19 retry_config: RetryConfig,
20}
21
22impl HttpClient {
23 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 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 pub fn with_retry_config(mut self, retry_config: RetryConfig) -> Self {
67 self.retry_config = retry_config;
68 self
69 }
70
71 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 pub fn post(&self, url: &str) -> RequestBuilder {
80 self.client.post(url)
81 }
82
83 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 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 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 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 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 for (key, value) in headers {
139 request = request.header(key, value);
140 }
141
142 if let Some(body) = body {
144 request = request.json(body);
145 }
146
147 self.execute_request_with_retry(request).await
148 }
149
150 fn build_url(&self, path: &str) -> Result<String> {
152 let mut url = self.config.base_url.clone();
153
154 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 if !url.ends_with('/') && !path.starts_with('/') {
164 url.push('/');
165 }
166 url.push_str(path);
167
168 Ok(url)
169 }
170
171 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 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 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 fn is_retryable_error(&self, response: &Response) -> bool {
229 match response.status().as_u16() {
230 500..=599 => true, 429 => true, 408 => true, _ => false,
235 }
236 }
237
238 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 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
252pub mod response {
254 use super::*;
255
256 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 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 pub fn is_success_status(status_code: u16) -> bool {
297 (200..300).contains(&status_code)
298 }
299
300 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
311pub mod oauth {
313 use super::*;
314
315 pub async fn token_exchange(
317 client: &HttpClient,
318 token_endpoint: &str,
319 params: &HashMap<String, String>,
320 ) -> Result<serde_json::Value> {
321 let path = if token_endpoint.starts_with("http") {
323 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 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, ¶ms).await?;
348 response::parse_json(response).await
349 }
350
351 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 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 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 async fn execute_absolute_url_form_post(
377 _client: &HttpClient,
378 url: &str,
379 params: &HashMap<String, String>,
380 ) -> Result<serde_json::Value> {
381 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
390pub mod webhooks {
392 use super::*;
393
394 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 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 fn calculate_webhook_signature(payload: &str, key: &str) -> Result<String> {
432 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