Skip to main content

batch_test/
batch_test.rs

1//! Batch test — 3 parallel headful browsers × 10 rounds with proxy rotation.
2//! Rotate IP (5s cooldown) → launch 3 browsers → browsers live 30s → close → repeat.
3
4use clawser_browser::Browser;
5use std::time::Instant;
6
7const PER_ROUND: usize = 3;
8const ROUNDS: usize = 10;
9const BROWSER_LIVE_SECS: u64 = 30;
10const ROTATE_COOLDOWN_SECS: u64 = 5;
11
12const PROXY_KEY: &str = "h9f015c1zpu65df90452b6349a63a9ac2d7bf15be1727e453ab6013";
13
14const USERS: [&str; 30] = [
15    "chokevy", "phanminhtu", "lehoangnam", "nguyenvana",
16    "tranthimai", "vothanhdat", "buiquochuy", "dangtuanhung",
17    "lythihuong", "hoanganhduc", "ngothikieu", "phamduclong",
18    "truongmylinh", "doanthanhson", "luuquanghai", "nguyenthilan",
19    "vutrongnhan", "lequangvinh", "tranductho", "phamthingoc",
20    "hothienphuc", "nguyenkhoa", "vuthanhtrung", "buithihoa",
21    "dangquocbao", "lethimynhi", "phanducminh", "truongankhang",
22    "nguyenhaiyen", "vothianhthy",
23];
24
25fn api_url() -> String {
26    std::env::var("GAME_API_URL").unwrap_or_else(|_| "https://api.abb1211.com/endpoint/play".into())
27}
28fn api_token() -> String {
29    std::env::var("GAME_API_TOKEN").expect("Set GAME_API_TOKEN in .env")
30}
31
32fn load_dotenv() {
33    for path in ["clawser-browser/.env", ".env"] {
34        if let Ok(content) = std::fs::read_to_string(path) {
35            for line in content.lines() {
36                let line = line.trim();
37                if line.is_empty() || line.starts_with('#') { continue; }
38                if let Some((k, v)) = line.split_once('=') {
39                    std::env::set_var(k.trim(), v.trim());
40                }
41            }
42            break;
43        }
44    }
45}
46
47async fn rotate_and_get_proxy() -> Option<String> {
48    let client = reqwest::Client::new();
49
50    // Rotate
51    let url = format!("https://api.zingproxy.com/open/change-ip/{}", PROXY_KEY);
52    match client.get(&url).send().await {
53        Ok(resp) => {
54            let body: serde_json::Value = resp.json().await.unwrap_or_default();
55            let msg = body["message"].as_str().unwrap_or("?");
56            println!("  Rotate: {msg}");
57        }
58        Err(e) => { println!("  Rotate FAIL: {e}"); return None; }
59    }
60
61    tokio::time::sleep(std::time::Duration::from_secs(ROTATE_COOLDOWN_SECS)).await;
62
63    // Get proxy — re-fetch to get new IP after rotation
64    let url = format!("https://api.zingproxy.com/open/get-proxy/{}", PROXY_KEY);
65    match client.get(&url).send().await {
66        Ok(resp) => {
67            let body: serde_json::Value = resp.json().await.unwrap_or_default();
68            // Try HTTP proxy first (more compatible), fallback to SOCKS5
69            if let Some(http) = body["proxy"]["httpProxy"].as_str() {
70                let parts: Vec<&str> = http.splitn(4, ':').collect();
71                if parts.len() >= 2 {
72                    let proxy = format!("http://{}:{}", parts[0], parts[1]);
73                    println!("  Proxy: {proxy}");
74                    return Some(proxy);
75                }
76            }
77            println!("  Get proxy FAIL: {body}");
78            None
79        }
80        Err(e) => { println!("  Get proxy FAIL: {e}"); None }
81    }
82}
83
84/// Verify proxy works before launching browsers.
85async fn verify_proxy(proxy: &str) -> bool {
86    let proxy_obj = match reqwest::Proxy::all(proxy) {
87        Ok(p) => p,
88        Err(e) => { println!("  Verify FAIL (bad proxy url): {e}"); return false; }
89    };
90    let client = match reqwest::Client::builder()
91        .proxy(proxy_obj)
92        .timeout(std::time::Duration::from_secs(10))
93        .build()
94    {
95        Ok(c) => c,
96        Err(e) => { println!("  Verify FAIL (client): {e}"); return false; }
97    };
98    // Use http (not https) to avoid TLS-over-proxy issues during verify
99    match client.get("http://httpbin.org/ip").send().await {
100        Ok(resp) => {
101            let body = resp.text().await.unwrap_or_default();
102            println!("  Verify OK: {}", body.trim());
103            true
104        }
105        Err(e) => { println!("  Verify FAIL: {e}"); false }
106    }
107}
108
109#[tokio::main]
110async fn main() {
111    load_dotenv();
112    let total = PER_ROUND * ROUNDS;
113    println!("=== Proxy batch: {}×{} = {} | live {}s | rotate {}s cooldown ===\n",
114        PER_ROUND, ROUNDS, total, BROWSER_LIVE_SECS, ROTATE_COOLDOWN_SECS);
115
116    let start = Instant::now();
117    let mut ok = 0u32;
118    let mut fail = 0u32;
119
120    for round in 0..ROUNDS {
121        println!("--- Round {}/{} ---", round + 1, ROUNDS);
122
123        // 1. Rotate IP and get proxy
124        let proxy = match rotate_and_get_proxy().await {
125            Some(p) => p,
126            None => {
127                println!("  Skip round — no proxy");
128                fail += PER_ROUND as u32;
129                continue;
130            }
131        };
132
133        // 2. Verify proxy works
134        if !verify_proxy(&proxy).await {
135            println!("  Skip round — proxy dead");
136            fail += PER_ROUND as u32;
137            continue;
138        }
139
140        // 3. Launch 3 browsers in parallel
141        let offset = round * PER_ROUND;
142        let mut handles = Vec::new();
143
144        for i in 0..PER_ROUND {
145            let idx = offset + i;
146            let user_id = USERS[idx % USERS.len()].to_string();
147            let proxy = proxy.clone();
148            let api = api_url();
149            let token = api_token();
150
151            handles.push(tokio::spawn(async move {
152                let label = format!("[{:02}/{}]", idx + 1, total);
153                let t = Instant::now();
154
155                // Get game URL (direct, no proxy)
156                let game_url = match reqwest::Client::new()
157                    .post(&api)
158                    .header("Authorization", format!("Bearer {}", token))
159                    .header("Content-Type", "application/json")
160                    .body(format!(r#"{{"user_id": "{}"}}"#, user_id))
161                    .send().await
162                {
163                    Ok(resp) => {
164                        let body: serde_json::Value = resp.json().await.unwrap_or_default();
165                        match body["url"].as_str() {
166                            Some(u) if !u.is_empty() => u.to_string(),
167                            _ => return format!("{label} FAIL api: {body}"),
168                        }
169                    }
170                    Err(e) => return format!("{label} FAIL api: {e}"),
171                };
172                let api_ms = t.elapsed().as_millis();
173
174                // Launch browser with proxy
175                let browser = match Browser::builder()
176                    .headful()
177                    .random()
178                    .proxy(&proxy)
179                    .build().await
180                {
181                    Ok(b) => b,
182                    Err(e) => return format!("{label} FAIL launch: {e}"),
183                };
184                let launch_ms = t.elapsed().as_millis() - api_ms;
185
186                // Navigate (human simulation auto-starts with aggressive scroll)
187                let page = match browser.new_page("about:blank").await {
188                    Ok(p) => p,
189                    Err(e) => return format!("{label} FAIL page: {e}"),
190                };
191
192                // Aggressive scrolling in background
193                let scroll_page = page.cdp().clone();
194                let scroll_handle = tokio::spawn(async move {
195                    use chromiumoxide::cdp::browser_protocol::input::{
196                        DispatchMouseEventParams, DispatchMouseEventType,
197                    };
198                    let mut y = 300.0f64;
199                    loop {
200                        // Scroll down aggressively
201                        let mut ev = DispatchMouseEventParams::new(
202                            DispatchMouseEventType::MouseWheel, 500.0, y,
203                        );
204                        ev.delta_x = Some(0.0);
205                        ev.delta_y = Some(300.0);
206                        let _ = scroll_page.execute(ev).await;
207                        y = ((y + 50.0) % 800.0) + 100.0;
208                        tokio::time::sleep(std::time::Duration::from_millis(800)).await;
209                    }
210                });
211
212                let t_nav = Instant::now();
213                if let Err(e) = page.goto(&game_url).await {
214                    scroll_handle.abort();
215                    return format!("{label} FAIL nav: {e}");
216                }
217                let nav_ms = t_nav.elapsed().as_millis();
218
219                // Wait for cookies
220                tokio::time::sleep(std::time::Duration::from_secs(5)).await;
221
222                let cookies = page.cdp().get_cookies().await.unwrap_or_default();
223                let has_evo = cookies.iter().any(|c| c.name == "EVOSESSIONID");
224                let evo = cookies.iter().find(|c| c.name == "EVOSESSIONID")
225                    .map(|c| c.value.clone())
226                    .unwrap_or_else(|| "N/A".into());
227                let cookie_names: Vec<String> = cookies.iter().map(|c| c.name.clone()).collect();
228
229                // Stay alive for remaining time (human sim + aggressive scroll running)
230                let elapsed = t.elapsed().as_secs();
231                if elapsed < BROWSER_LIVE_SECS {
232                    tokio::time::sleep(std::time::Duration::from_secs(BROWSER_LIVE_SECS - elapsed)).await;
233                }
234
235                // Close browser
236                scroll_handle.abort();
237                let _ = browser.close().await;
238                let total_ms = t.elapsed().as_millis();
239
240                let status = if has_evo { "OK" } else { "MISS" };
241                format!("{label} {status} | {user_id:<18} | total:{total_ms}ms api:{api_ms}ms launch:{launch_ms}ms nav:{nav_ms}ms | cookies:{} | EVO:{evo}\n       {:?}",
242                    cookies.len(), cookie_names)
243            }));
244        }
245
246        for h in handles {
247            match h.await {
248                Ok(msg) => {
249                    if msg.contains("] OK ") { ok += 1; } else { fail += 1; }
250                    println!("{msg}");
251                }
252                Err(e) => { println!("  task panic: {e}"); fail += 1; }
253            }
254        }
255
256        println!();
257    }
258
259    let elapsed = start.elapsed().as_secs_f64();
260    println!("=== OK:{ok} FAIL:{fail} | {elapsed:.0}s total | {:.1}s/profile ===",
261        elapsed / total as f64);
262}