Skip to main content

steam_auth/
http_client.rs

1//! HTTP client for Steam web authentication.
2//!
3//! This module provides an `HttpClient` implementation using reqwest.
4
5use std::collections::HashMap;
6
7use crate::error::SessionError;
8
9/// HTTP response from a request.
10#[derive(Debug, Clone)]
11pub struct HttpResponse {
12    /// HTTP status code.
13    pub status: u16,
14    /// Response headers.
15    pub headers: HashMap<String, String>,
16    /// Response body as bytes.
17    pub body: Vec<u8>,
18}
19
20impl HttpResponse {
21    /// Check if the response status is successful (2xx).
22    pub fn is_success(&self) -> bool {
23        (200..300).contains(&self.status)
24    }
25
26    /// Parse the body as JSON.
27    pub fn json<T: serde::de::DeserializeOwned>(&self) -> Result<T, SessionError> {
28        serde_json::from_slice(&self.body).map_err(|e| SessionError::NetworkError(format!("JSON parse error: {}", e)))
29    }
30
31    /// Get a header value by name (case-insensitive).
32    pub fn get_header(&self, name: &str) -> Option<&String> {
33        let lower = name.to_lowercase();
34        self.headers.iter().find(|(k, _)| k.to_lowercase() == lower).map(|(_, v)| v)
35    }
36
37    /// Get all values for a header (for multi-value headers like Set-Cookie).
38    pub fn get_all_headers(&self, name: &str) -> Vec<&String> {
39        let lower = name.to_lowercase();
40        self.headers.iter().filter(|(k, _)| k.to_lowercase() == lower).map(|(_, v)| v).collect()
41    }
42}
43
44/// Multipart form field.
45#[derive(Debug, Clone)]
46pub struct FormField {
47    /// Field name.
48    pub name: String,
49    /// Field value.
50    pub value: String,
51}
52
53/// Multipart form for HTTP requests.
54#[derive(Debug, Clone, Default)]
55pub struct MultipartForm {
56    /// Form fields.
57    pub fields: Vec<FormField>,
58}
59
60impl MultipartForm {
61    /// Create a new empty form.
62    pub fn new() -> Self {
63        Self::default()
64    }
65
66    /// Add a text field to the form.
67    pub fn text(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
68        self.fields.push(FormField { name: name.into(), value: value.into() });
69        self
70    }
71}
72
73/// Production HTTP client using reqwest.
74#[derive(Debug, Clone)]
75pub struct HttpClient {
76    client: reqwest::Client,
77}
78
79impl HttpClient {
80    /// Create a new HTTP client.
81    pub fn new() -> Self {
82        Self { client: reqwest::Client::new() }
83    }
84
85    /// Create a new HTTP client with a custom reqwest client.
86    pub fn with_client(client: reqwest::Client) -> Self {
87        Self { client }
88    }
89
90    /// Send a POST request with multipart form data.
91    pub async fn post_multipart(&self, url: &str, form: MultipartForm, headers: HashMap<String, String>) -> Result<HttpResponse, SessionError> {
92        let mut req_form = reqwest::multipart::Form::new();
93        for field in form.fields {
94            req_form = req_form.text(field.name, field.value);
95        }
96
97        let mut req = self.client.post(url).multipart(req_form);
98
99        for (key, value) in &headers {
100            req = req.header(key, value);
101        }
102
103        let response = req.send().await?;
104        let status = response.status().as_u16();
105
106        // Collect headers, handling multi-value headers
107        let mut response_headers = HashMap::new();
108        for (name, value) in response.headers() {
109            if let Ok(v) = value.to_str() {
110                // For Set-Cookie and similar, we need special handling
111                // For now, just store the last value (or combine them)
112                let key = name.to_string();
113                response_headers.entry(key).and_modify(|existing: &mut String| existing.push_str(&format!(", {}", v))).or_insert_with(|| v.to_string());
114            }
115        }
116
117        let body = response.bytes().await?.to_vec();
118
119        Ok(HttpResponse { status, headers: response_headers, body })
120    }
121
122    /// Send a POST request with JSON body.
123    pub async fn post_json(&self, url: &str, body: &[u8], headers: HashMap<String, String>) -> Result<HttpResponse, SessionError> {
124        let mut req = self.client.post(url).body(body.to_vec());
125
126        for (key, value) in &headers {
127            req = req.header(key, value);
128        }
129
130        let response = req.send().await?;
131        let status = response.status().as_u16();
132
133        let mut response_headers = HashMap::new();
134        for (name, value) in response.headers() {
135            if let Ok(v) = value.to_str() {
136                response_headers.insert(name.to_string(), v.to_string());
137            }
138        }
139
140        let body = response.bytes().await?.to_vec();
141
142        Ok(HttpResponse { status, headers: response_headers, body })
143    }
144}
145
146impl Default for HttpClient {
147    fn default() -> Self {
148        Self::new()
149    }
150}