1use std::{
18 collections::{HashMap, VecDeque},
19 sync::{Arc, Mutex},
20};
21
22use async_trait::async_trait;
23
24use crate::error::SteamError;
25
26#[derive(Debug, Clone)]
28pub struct HttpResponse {
29 pub status: u16,
31 pub body: Vec<u8>,
33 pub headers: HashMap<String, String>,
35}
36
37impl HttpResponse {
38 pub fn ok(body: Vec<u8>) -> Self {
40 Self { status: 200, body, headers: HashMap::new() }
41 }
42
43 pub fn error(status: u16, body: Vec<u8>) -> Self {
45 Self { status, body, headers: HashMap::new() }
46 }
47
48 pub fn is_success(&self) -> bool {
50 (200..300).contains(&self.status)
51 }
52
53 pub fn json<T: serde::de::DeserializeOwned>(&self) -> Result<T, SteamError> {
55 serde_json::from_slice(&self.body).map_err(|e| SteamError::ProtocolError(format!("Failed to parse JSON: {}", e)))
56 }
57
58 pub fn text(&self) -> Result<String, SteamError> {
60 String::from_utf8(self.body.clone()).map_err(|e| SteamError::ProtocolError(format!("Invalid UTF-8: {}", e)))
61 }
62}
63
64#[async_trait]
69pub trait HttpClient: Send + Sync {
70 async fn get(&self, url: &str) -> Result<HttpResponse, SteamError>;
75
76 async fn get_with_query(&self, url: &str, query: &[(&str, &str)]) -> Result<HttpResponse, SteamError>;
82
83 async fn post(&self, url: &str, body: Vec<u8>, content_type: &str) -> Result<HttpResponse, SteamError>;
90
91 async fn post_form(&self, url: &str, form: &[(&str, &str)]) -> Result<HttpResponse, SteamError>;
97
98 async fn get_with_cookies(&self, url: &str, cookies: &str) -> Result<HttpResponse, SteamError> {
108 let _ = cookies;
109 self.get(url).await
110 }
111
112 async fn post_form_with_cookies(&self, url: &str, form: &[(&str, &str)], cookies: &str) -> Result<HttpResponse, SteamError> {
118 let _ = cookies;
119 self.post_form(url, form).await
120 }
121}
122
123#[derive(Clone)]
127pub struct ReqwestHttpClient {
128 client: reqwest::Client,
129}
130
131impl ReqwestHttpClient {
132 pub fn new() -> Self {
140 use std::time::Duration;
141
142 use reqwest::header::{HeaderMap, HeaderValue, ACCEPT, ACCEPT_CHARSET, USER_AGENT};
143
144 let mut headers = HeaderMap::new();
145 headers.insert(USER_AGENT, HeaderValue::from_static("Valve/Steam HTTP Client 1.0"));
146 headers.insert(ACCEPT, HeaderValue::from_static("text/html,*/*;q=0.9"));
147 headers.insert(ACCEPT_CHARSET, HeaderValue::from_static("ISO-8859-1,utf-8,*;q=0.7"));
148
149 let client = reqwest::Client::builder().default_headers(headers).pool_max_idle_per_host(10).pool_idle_timeout(Duration::from_secs(60)).connect_timeout(Duration::from_secs(30)).timeout(Duration::from_secs(30)).gzip(true).build().unwrap_or_else(|_| reqwest::Client::new());
151
152 Self { client }
153 }
154
155 pub fn with_client(client: reqwest::Client) -> Self {
157 Self { client }
158 }
159
160 async fn convert_response(resp: reqwest::Response) -> Result<HttpResponse, SteamError> {
162 let status = resp.status().as_u16();
163 let mut headers = HashMap::new();
164
165 for (key, value) in resp.headers() {
166 if let Ok(v) = value.to_str() {
167 headers.insert(key.as_str().to_lowercase(), v.to_string());
168 }
169 }
170
171 let body = resp.bytes().await.map_err(|e| SteamError::NetworkError(std::io::Error::other(e)))?.to_vec();
172
173 Ok(HttpResponse { status, body, headers })
174 }
175}
176
177impl Default for ReqwestHttpClient {
178 fn default() -> Self {
179 Self::new()
180 }
181}
182
183#[async_trait]
184impl HttpClient for ReqwestHttpClient {
185 async fn get(&self, url: &str) -> Result<HttpResponse, SteamError> {
186 let resp = self.client.get(url).send().await.map_err(|e| SteamError::NetworkError(std::io::Error::other(e)))?;
187
188 Self::convert_response(resp).await
189 }
190
191 async fn get_with_query(&self, url: &str, query: &[(&str, &str)]) -> Result<HttpResponse, SteamError> {
192 let resp = self.client.get(url).query(query).send().await.map_err(|e| SteamError::NetworkError(std::io::Error::other(e)))?;
193
194 Self::convert_response(resp).await
195 }
196
197 async fn post(&self, url: &str, body: Vec<u8>, content_type: &str) -> Result<HttpResponse, SteamError> {
198 let resp = self.client.post(url).header("Content-Type", content_type).body(body).send().await.map_err(|e| SteamError::NetworkError(std::io::Error::other(e)))?;
199
200 Self::convert_response(resp).await
201 }
202
203 async fn post_form(&self, url: &str, form: &[(&str, &str)]) -> Result<HttpResponse, SteamError> {
204 let resp = self.client.post(url).form(form).send().await.map_err(|e| SteamError::NetworkError(std::io::Error::other(e)))?;
205
206 Self::convert_response(resp).await
207 }
208
209 async fn get_with_cookies(&self, url: &str, cookies: &str) -> Result<HttpResponse, SteamError> {
210 let client = build_cookie_client(cookies)?;
211 let resp = client.get(url).send().await.map_err(|e| SteamError::NetworkError(std::io::Error::other(e)))?;
212 Self::convert_response(resp).await
213 }
214
215 async fn post_form_with_cookies(&self, url: &str, form: &[(&str, &str)], cookies: &str) -> Result<HttpResponse, SteamError> {
216 let client = build_cookie_client(cookies)?;
217 let resp = client.post(url).form(form).send().await.map_err(|e| SteamError::NetworkError(std::io::Error::other(e)))?;
218 Self::convert_response(resp).await
219 }
220}
221
222fn build_cookie_client(cookies: &str) -> Result<reqwest::Client, SteamError> {
225 use std::time::Duration;
226
227 use reqwest::header::{HeaderMap, HeaderValue, COOKIE};
228
229 let mut cookie_value: HeaderValue = cookies.parse().map_err(|_| SteamError::Other("Invalid cookie header".to_string()))?;
230 cookie_value.set_sensitive(true);
231
232 let mut headers = HeaderMap::new();
233 headers.insert(COOKIE, cookie_value);
234
235 reqwest::Client::builder()
236 .user_agent("Valve/Steam HTTP Client 1.0")
237 .default_headers(headers)
238 .connect_timeout(Duration::from_secs(30))
239 .timeout(Duration::from_secs(10))
240 .build()
241 .map_err(|e| SteamError::Other(format!("Failed to build HTTP client: {}", e)))
242}
243
244#[async_trait]
245impl steam_cm_provider::HttpClient for dyn HttpClient {
246 async fn get_with_query(&self, url: &str, query: &[(&str, &str)]) -> Result<steam_cm_provider::HttpResponse, steam_cm_provider::CmError> {
247 let resp = self.get_with_query(url, query).await.map_err(|e| steam_cm_provider::CmError::Network(e.to_string()))?;
248
249 Ok(steam_cm_provider::HttpResponse { status: resp.status, body: resp.body })
250 }
251}
252
253#[async_trait]
254impl steam_cm_provider::HttpClient for ReqwestHttpClient {
255 async fn get_with_query(&self, url: &str, query: &[(&str, &str)]) -> Result<steam_cm_provider::HttpResponse, steam_cm_provider::CmError> {
256 let resp = crate::utils::http::HttpClient::get_with_query(self, url, query).await.map_err(|e| steam_cm_provider::CmError::Network(e.to_string()))?;
257
258 Ok(steam_cm_provider::HttpResponse { status: resp.status, body: resp.body })
259 }
260}
261
262pub struct MockHttpClient {
280 responses: Arc<Mutex<VecDeque<Result<HttpResponse, SteamError>>>>,
282 requests: Arc<Mutex<Vec<MockRequest>>>,
284}
285
286#[derive(Debug, Clone)]
288pub struct MockRequest {
289 pub method: String,
291 pub url: String,
293 pub query: Vec<(String, String)>,
295 pub body: Option<Vec<u8>>,
297 pub content_type: Option<String>,
299}
300
301impl MockHttpClient {
302 pub fn new() -> Self {
304 Self { responses: Arc::new(Mutex::new(VecDeque::new())), requests: Arc::new(Mutex::new(Vec::new())) }
305 }
306
307 pub fn queue_response(&self, response: HttpResponse) {
309 self.responses.lock().expect("failed to get mock value").push_back(Ok(response));
310 }
311
312 pub fn queue_error(&self, error: SteamError) {
314 self.responses.lock().expect("failed to get mock value").push_back(Err(error));
315 }
316
317 pub fn queue_responses(&self, responses: Vec<HttpResponse>) {
319 let mut queue = self.responses.lock().expect("failed to get mock value");
320 for resp in responses {
321 queue.push_back(Ok(resp));
322 }
323 }
324
325 pub fn requests(&self) -> Vec<MockRequest> {
327 self.requests.lock().expect("failed to get mock value").clone()
328 }
329
330 pub fn last_request(&self) -> Option<MockRequest> {
332 self.requests.lock().expect("failed to get mock value").last().cloned()
333 }
334
335 pub fn clear_requests(&self) {
337 self.requests.lock().expect("failed to get mock value").clear();
338 }
339
340 pub fn request_count(&self) -> usize {
342 self.requests.lock().expect("failed to get mock value").len()
343 }
344
345 fn pop_response(&self) -> Result<HttpResponse, SteamError> {
347 self.responses.lock().expect("failed to get mock value").pop_front().unwrap_or_else(|| Err(SteamError::Other("MockHttpClient: No response queued".to_string())))
348 }
349
350 fn record_request(&self, request: MockRequest) {
352 self.requests.lock().expect("failed to get mock value").push(request);
353 }
354}
355
356impl Default for MockHttpClient {
357 fn default() -> Self {
358 Self::new()
359 }
360}
361
362impl Clone for MockHttpClient {
363 fn clone(&self) -> Self {
364 Self { responses: Arc::clone(&self.responses), requests: Arc::clone(&self.requests) }
365 }
366}
367
368#[async_trait]
369impl HttpClient for MockHttpClient {
370 async fn get(&self, url: &str) -> Result<HttpResponse, SteamError> {
371 self.record_request(MockRequest { method: "GET".to_string(), url: url.to_string(), query: Vec::new(), body: None, content_type: None });
372 self.pop_response()
373 }
374
375 async fn get_with_query(&self, url: &str, query: &[(&str, &str)]) -> Result<HttpResponse, SteamError> {
376 self.record_request(MockRequest {
377 method: "GET".to_string(),
378 url: url.to_string(),
379 query: query.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect(),
380 body: None,
381 content_type: None,
382 });
383 self.pop_response()
384 }
385
386 async fn post(&self, url: &str, body: Vec<u8>, content_type: &str) -> Result<HttpResponse, SteamError> {
387 self.record_request(MockRequest {
388 method: "POST".to_string(),
389 url: url.to_string(),
390 query: Vec::new(),
391 body: Some(body),
392 content_type: Some(content_type.to_string()),
393 });
394 self.pop_response()
395 }
396
397 async fn post_form(&self, url: &str, form: &[(&str, &str)]) -> Result<HttpResponse, SteamError> {
398 let form_body: String = form.iter().map(|(k, v)| format!("{}={}", k, v)).collect::<Vec<_>>().join("&");
400
401 self.record_request(MockRequest {
402 method: "POST".to_string(),
403 url: url.to_string(),
404 query: form.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect(),
405 body: Some(form_body.into_bytes()),
406 content_type: Some("application/x-www-form-urlencoded".to_string()),
407 });
408 self.pop_response()
409 }
410}
411
412#[cfg(test)]
413mod tests {
414 use super::*;
415
416 #[tokio::test]
417 async fn test_mock_http_client_get() {
418 let mock = MockHttpClient::new();
419 mock.queue_response(HttpResponse::ok(b"test response".to_vec()));
420
421 let response = mock.get("https://example.com/test").await.expect("failed to get mock value");
422
423 assert_eq!(response.status, 200);
424 assert_eq!(response.body, b"test response");
425 assert!(response.is_success());
426
427 let requests = mock.requests();
428 assert_eq!(requests.len(), 1);
429 assert_eq!(requests[0].method, "GET");
430 assert_eq!(requests[0].url, "https://example.com/test");
431 }
432
433 #[tokio::test]
434 async fn test_mock_http_client_get_with_query() {
435 let mock = MockHttpClient::new();
436 mock.queue_response(HttpResponse::ok(b"{}".to_vec()));
437
438 let query = [("key", "value"), ("foo", "bar")];
439 let response = mock.get_with_query("https://api.example.com", &query).await.expect("failed to get mock value");
440
441 assert!(response.is_success());
442
443 let request = mock.last_request().expect("failed to get mock value");
444 assert_eq!(request.query.len(), 2);
445 assert_eq!(request.query[0], ("key".to_string(), "value".to_string()));
446 }
447
448 #[tokio::test]
449 async fn test_mock_http_client_post() {
450 let mock = MockHttpClient::new();
451 mock.queue_response(HttpResponse::ok(b"created".to_vec()));
452
453 let body = b"request body".to_vec();
454 let response = mock.post("https://api.example.com/create", body.clone(), "text/plain").await.expect("failed to get mock value");
455
456 assert!(response.is_success());
457
458 let request = mock.last_request().expect("failed to get mock value");
459 assert_eq!(request.method, "POST");
460 assert_eq!(request.body, Some(body));
461 assert_eq!(request.content_type, Some("text/plain".to_string()));
462 }
463
464 #[tokio::test]
465 async fn test_mock_http_client_error() {
466 let mock = MockHttpClient::new();
467 mock.queue_error(SteamError::Timeout);
468
469 let result = mock.get("https://example.com").await;
470 assert!(result.is_err());
471 }
472
473 #[tokio::test]
474 async fn test_mock_http_client_no_response_queued() {
475 let mock = MockHttpClient::new();
476
477 let result = mock.get("https://example.com").await;
478 assert!(result.is_err());
479 }
480
481 #[tokio::test]
482 async fn test_http_response_json() {
483 let response = HttpResponse::ok(br#"{"key": "value"}"#.to_vec());
484 let json: serde_json::Value = response.json().expect("failed to get mock value");
485
486 assert_eq!(json["key"], "value");
487 }
488
489 #[tokio::test]
490 async fn test_http_response_is_success() {
491 assert!(HttpResponse::ok(vec![]).is_success());
492 assert!(HttpResponse { status: 201, body: vec![], headers: HashMap::new() }.is_success());
493 assert!(!HttpResponse::error(404, vec![]).is_success());
494 assert!(!HttpResponse::error(500, vec![]).is_success());
495 }
496}