Skip to main content

chaser_cf/models/
mod.rs

1//! Data models for chaser-cf
2
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6/// Stealth profile for browser fingerprinting
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
8#[serde(rename_all = "lowercase")]
9#[repr(C)]
10pub enum Profile {
11    /// Windows fingerprint (most common, default)
12    #[default]
13    Windows,
14    /// Linux fingerprint
15    Linux,
16    /// macOS fingerprint
17    Macos,
18}
19
20impl Profile {
21    /// Parse profile from string (for FFI)
22    pub fn parse(s: &str) -> Option<Self> {
23        match s.to_lowercase().as_str() {
24            "windows" | "win" => Some(Profile::Windows),
25            "linux" => Some(Profile::Linux),
26            "macos" | "mac" | "darwin" => Some(Profile::Macos),
27            _ => None,
28        }
29    }
30}
31
32/// Proxy configuration
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct ProxyConfig {
35    /// Proxy host
36    pub host: String,
37    /// Proxy port
38    pub port: u16,
39    /// Optional username for authentication
40    pub username: Option<String>,
41    /// Optional password for authentication
42    pub password: Option<String>,
43}
44
45impl ProxyConfig {
46    /// Create new proxy config
47    pub fn new(host: impl Into<String>, port: u16) -> Self {
48        Self {
49            host: host.into(),
50            port,
51            username: None,
52            password: None,
53        }
54    }
55
56    /// Add authentication credentials
57    pub fn with_auth(mut self, username: impl Into<String>, password: impl Into<String>) -> Self {
58        self.username = Some(username.into());
59        self.password = Some(password.into());
60        self
61    }
62
63    /// Get proxy URL in format http://host:port
64    /// Note: Chrome/CDP doesn't support inline authentication in proxy URLs
65    pub fn to_url(&self) -> String {
66        format!("http://{}:{}", self.host, self.port)
67    }
68}
69
70/// Browser cookie
71#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct Cookie {
73    /// Cookie name
74    pub name: String,
75    /// Cookie value
76    pub value: String,
77    /// Cookie domain
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub domain: Option<String>,
80    /// Cookie path
81    #[serde(skip_serializing_if = "Option::is_none")]
82    pub path: Option<String>,
83    /// Expiration timestamp
84    #[serde(skip_serializing_if = "Option::is_none")]
85    pub expires: Option<f64>,
86    /// HTTP only flag
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub http_only: Option<bool>,
89    /// Secure flag
90    #[serde(skip_serializing_if = "Option::is_none")]
91    pub secure: Option<bool>,
92    /// SameSite attribute
93    #[serde(skip_serializing_if = "Option::is_none")]
94    pub same_site: Option<String>,
95}
96
97impl Cookie {
98    /// Create a simple cookie with name and value
99    pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
100        Self {
101            name: name.into(),
102            value: value.into(),
103            domain: None,
104            path: None,
105            expires: None,
106            http_only: None,
107            secure: None,
108            same_site: None,
109        }
110    }
111}
112
113/// WAF session data containing cookies and headers
114#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct WafSession {
116    /// Extracted cookies
117    pub cookies: Vec<Cookie>,
118    /// Extracted headers (cleaned)
119    pub headers: HashMap<String, String>,
120}
121
122impl WafSession {
123    /// Create new WAF session
124    pub fn new(cookies: Vec<Cookie>, headers: HashMap<String, String>) -> Self {
125        Self { cookies, headers }
126    }
127
128    /// Get cookies as a single cookie header string
129    pub fn cookies_string(&self) -> String {
130        self.cookies
131            .iter()
132            .map(|c| format!("{}={}", c.name, c.value))
133            .collect::<Vec<_>>()
134            .join("; ")
135    }
136}
137
138/// Result of a chaser-cf operation (for FFI serialization)
139#[derive(Debug, Clone, Serialize, Deserialize)]
140#[serde(tag = "type", content = "data")]
141pub enum ChaserResult {
142    /// Page source HTML
143    Source(String),
144    /// Turnstile token
145    Token(String),
146    /// WAF session
147    WafSession(WafSession),
148    /// Error
149    Error { code: i32, message: String },
150}
151
152impl ChaserResult {
153    /// Create success result with source
154    pub fn source(html: String) -> Self {
155        ChaserResult::Source(html)
156    }
157
158    /// Create success result with token
159    pub fn token(token: String) -> Self {
160        ChaserResult::Token(token)
161    }
162
163    /// Create success result with WAF session
164    pub fn waf_session(session: WafSession) -> Self {
165        ChaserResult::WafSession(session)
166    }
167
168    /// Create error result
169    pub fn error(code: i32, message: impl Into<String>) -> Self {
170        ChaserResult::Error {
171            code,
172            message: message.into(),
173        }
174    }
175
176    /// Check if result is success
177    pub fn is_success(&self) -> bool {
178        !matches!(self, ChaserResult::Error { .. })
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185
186    #[test]
187    fn test_proxy_url_without_auth() {
188        let proxy = ProxyConfig::new("proxy.example.com", 8080);
189        assert_eq!(proxy.to_url(), "http://proxy.example.com:8080");
190    }
191
192    #[test]
193    fn test_proxy_url_with_auth() {
194        let proxy = ProxyConfig::new("proxy.example.com", 8080).with_auth("user", "pass");
195        assert_eq!(proxy.to_url(), "http://user:pass@proxy.example.com:8080");
196    }
197
198    #[test]
199    fn test_waf_session_cookies_string() {
200        let session = WafSession::new(
201            vec![
202                Cookie::new("cf_clearance", "abc123"),
203                Cookie::new("session", "xyz789"),
204            ],
205            HashMap::new(),
206        );
207        assert_eq!(
208            session.cookies_string(),
209            "cf_clearance=abc123; session=xyz789"
210        );
211    }
212
213    #[test]
214    fn test_profile_from_str() {
215        assert_eq!(Profile::parse("windows"), Some(Profile::Windows));
216        assert_eq!(Profile::parse("LINUX"), Some(Profile::Linux));
217        assert_eq!(Profile::parse("darwin"), Some(Profile::Macos));
218        assert_eq!(Profile::parse("invalid"), None);
219    }
220}