1use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
8#[serde(rename_all = "lowercase")]
9#[repr(C)]
10pub enum Profile {
11 #[default]
13 Windows,
14 Linux,
16 Macos,
18}
19
20impl Profile {
21 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#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct ProxyConfig {
35 pub host: String,
37 pub port: u16,
39 pub username: Option<String>,
41 pub password: Option<String>,
43 #[serde(default, skip_serializing_if = "Option::is_none")]
48 pub scheme: Option<String>,
49}
50
51impl ProxyConfig {
52 pub fn new(host: impl Into<String>, port: u16) -> Self {
54 Self {
55 host: host.into(),
56 port,
57 username: None,
58 password: None,
59 scheme: None,
60 }
61 }
62
63 pub fn with_auth(mut self, username: impl Into<String>, password: impl Into<String>) -> Self {
65 self.username = Some(username.into());
66 self.password = Some(password.into());
67 self
68 }
69
70 pub fn with_scheme(mut self, scheme: impl Into<String>) -> Self {
77 self.scheme = Some(scheme.into());
78 self
79 }
80
81 pub fn to_url(&self) -> String {
85 let scheme = self.scheme.as_deref().unwrap_or("http");
86 format!("{scheme}://{}:{}", self.host, self.port)
87 }
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct Cookie {
93 pub name: String,
95 pub value: String,
97 #[serde(skip_serializing_if = "Option::is_none")]
99 pub domain: Option<String>,
100 #[serde(skip_serializing_if = "Option::is_none")]
102 pub path: Option<String>,
103 #[serde(skip_serializing_if = "Option::is_none")]
105 pub expires: Option<f64>,
106 #[serde(skip_serializing_if = "Option::is_none")]
108 pub http_only: Option<bool>,
109 #[serde(skip_serializing_if = "Option::is_none")]
111 pub secure: Option<bool>,
112 #[serde(skip_serializing_if = "Option::is_none")]
114 pub same_site: Option<String>,
115}
116
117impl Cookie {
118 pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
120 Self {
121 name: name.into(),
122 value: value.into(),
123 domain: None,
124 path: None,
125 expires: None,
126 http_only: None,
127 secure: None,
128 same_site: None,
129 }
130 }
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct WafSession {
136 pub cookies: Vec<Cookie>,
138 pub headers: HashMap<String, String>,
140}
141
142impl WafSession {
143 pub fn new(cookies: Vec<Cookie>, headers: HashMap<String, String>) -> Self {
145 Self { cookies, headers }
146 }
147
148 pub fn cookies_string(&self) -> String {
150 self.cookies
151 .iter()
152 .map(|c| format!("{}={}", c.name, c.value))
153 .collect::<Vec<_>>()
154 .join("; ")
155 }
156}
157
158#[derive(Debug, Clone, Serialize, Deserialize)]
160#[serde(tag = "type", content = "data")]
161pub enum ChaserResult {
162 Source(String),
164 Token(String),
166 WafSession(WafSession),
168 Error { code: i32, message: String },
170}
171
172impl ChaserResult {
173 pub fn source(html: String) -> Self {
175 ChaserResult::Source(html)
176 }
177
178 pub fn token(token: String) -> Self {
180 ChaserResult::Token(token)
181 }
182
183 pub fn waf_session(session: WafSession) -> Self {
185 ChaserResult::WafSession(session)
186 }
187
188 pub fn error(code: i32, message: impl Into<String>) -> Self {
190 ChaserResult::Error {
191 code,
192 message: message.into(),
193 }
194 }
195
196 pub fn is_success(&self) -> bool {
198 !matches!(self, ChaserResult::Error { .. })
199 }
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205
206 #[test]
207 fn test_proxy_url_without_auth() {
208 let proxy = ProxyConfig::new("proxy.example.com", 8080);
209 assert_eq!(proxy.to_url(), "http://proxy.example.com:8080");
210 }
211
212 #[test]
213 fn test_proxy_url_with_auth() {
214 let proxy = ProxyConfig::new("proxy.example.com", 8080).with_auth("user", "pass");
215 assert_eq!(proxy.to_url(), "http://proxy.example.com:8080");
216 }
217
218 #[test]
219 fn test_proxy_url_with_socks5_scheme() {
220 let proxy = ProxyConfig::new("127.0.0.1", 1080).with_scheme("socks5");
221 assert_eq!(proxy.to_url(), "socks5://127.0.0.1:1080");
222 }
223
224 #[test]
225 fn test_proxy_url_with_socks5h_scheme_remote_dns() {
226 let proxy = ProxyConfig::new("127.0.0.1", 1080)
227 .with_scheme("socks5h")
228 .with_auth("u", "p");
229 assert_eq!(proxy.to_url(), "socks5h://127.0.0.1:1080");
230 }
231
232 #[test]
233 fn test_proxy_url_default_scheme_is_http() {
234 let proxy = ProxyConfig::new("h", 1);
238 assert!(proxy.scheme.is_none());
239 assert_eq!(proxy.to_url(), "http://h:1");
240 }
241
242 #[test]
243 fn test_waf_session_cookies_string() {
244 let session = WafSession::new(
245 vec![
246 Cookie::new("cf_clearance", "abc123"),
247 Cookie::new("session", "xyz789"),
248 ],
249 HashMap::new(),
250 );
251 assert_eq!(
252 session.cookies_string(),
253 "cf_clearance=abc123; session=xyz789"
254 );
255 }
256
257 #[test]
258 fn test_profile_from_str() {
259 assert_eq!(Profile::parse("windows"), Some(Profile::Windows));
260 assert_eq!(Profile::parse("LINUX"), Some(Profile::Linux));
261 assert_eq!(Profile::parse("darwin"), Some(Profile::Macos));
262 assert_eq!(Profile::parse("invalid"), None);
263 }
264}