1use std::collections::{HashMap, HashSet};
8
9use crate::error::SessionError;
10
11#[derive(Debug, Clone)]
13pub struct TransferInfo {
14 pub url: String,
16 pub nonce: Option<String>,
18 pub auth: Option<String>,
20}
21
22pub fn parse_transfer_info(json: &serde_json::Value) -> Result<Vec<TransferInfo>, SessionError> {
40 let transfer_info = json["transfer_info"].as_array().ok_or(SessionError::NetworkError("No transfer_info in response".into()))?;
41
42 let mut result = Vec::new();
43 for transfer in transfer_info {
44 let url = transfer["url"].as_str().ok_or(SessionError::NetworkError("Missing transfer URL".into()))?.to_string();
45
46 let params = &transfer["params"];
47 let nonce = params["nonce"].as_str().map(String::from);
48 let auth = params["auth"].as_str().map(String::from);
49
50 result.push(TransferInfo { url, nonce, auth });
51 }
52
53 Ok(result)
54}
55
56pub fn extract_domain_from_url(url: &str) -> Option<String> {
65 url::Url::parse(url).ok().and_then(|parsed| parsed.host_str().map(String::from))
66}
67
68pub fn build_cookie_with_domain(cookie_str: &str, url: &str) -> Option<String> {
81 let domain = extract_domain_from_url(url)?;
82
83 if cookie_str.to_lowercase().contains("domain=") {
84 Some(cookie_str.to_string())
85 } else {
86 Some(format!("{}; Domain={}", cookie_str, domain))
87 }
88}
89
90pub fn parse_cookies_from_header(header_value: &str, url: &str) -> Vec<String> {
101 let mut cookies = Vec::new();
102 let mut seen = std::collections::HashSet::new();
103
104 for line in header_value.split('\n') {
106 for cookie_str in line.split(", ") {
107 let cookie_str = cookie_str.trim();
108 if cookie_str.starts_with("steamLoginSecure=") {
109 if let Some(cookie) = build_cookie_with_domain(cookie_str, url) {
110 if !seen.contains(&cookie) {
112 seen.insert(cookie.clone());
113 cookies.push(cookie);
114 }
115 }
116 }
117 }
118 }
119
120 cookies
121}
122
123pub fn extract_cookie_domains(cookies: &[String]) -> HashSet<String> {
134 cookies.iter().filter_map(|c| c.split("Domain=").nth(1).map(|d| d.split(';').next().unwrap_or(d).to_string())).filter(|d| d != "login.steampowered.com").collect()
135}
136
137pub fn add_session_id_cookies(cookies: &mut Vec<String>, session_id: &str, domains: &HashSet<String>) {
146 for domain in domains {
147 cookies.push(format!("sessionid={}; Path=/; Secure; SameSite=None; Domain={}", session_id, domain));
148 }
149}
150
151pub fn filter_session_id_cookies(cookies: &mut Vec<String>) {
158 cookies.retain(|c| !c.starts_with("sessionid="));
159}
160
161pub fn build_simple_cookies(steam_id: u64, access_token: &str, session_id: &str) -> Vec<String> {
173 let cookie_value = format!("{}||{}", steam_id, access_token);
174 let encoded = urlencoding::encode(&cookie_value);
175
176 vec![format!("steamLoginSecure={}", encoded), format!("sessionid={}", session_id)]
177}
178
179pub fn check_finalize_error(json: &serde_json::Value) -> Result<(), SessionError> {
188 if let Some(error) = json.get("error") {
189 if !error.is_null() {
190 let eresult_val = json["eresult"].as_i64().or_else(|| json["error"].as_i64()).unwrap_or(2) as i32; return Err(SessionError::from_eresult(eresult_val, Some(error.to_string())));
193 }
194 }
195 Ok(())
196}
197pub fn extract_steam_login_cookies(cookie_header: &str, url: &str) -> Vec<String> {
217 let mut cookies = Vec::new();
218 let mut seen = std::collections::HashSet::new();
219
220 for line in cookie_header.split('\n') {
222 for cookie_str in line.split(", ") {
223 let cookie_str = cookie_str.trim();
224 if cookie_str.starts_with("steamLoginSecure=") {
225 if let Some(cookie) = build_cookie_with_domain(cookie_str, url) {
226 if !seen.contains(&cookie) {
228 seen.insert(cookie.clone());
229 cookies.push(cookie);
230 }
231 }
232 }
233 }
234 }
235
236 cookies
237}
238
239pub fn build_transfer_form_params(transfer: &TransferInfo, steam_id: u64) -> HashMap<String, String> {
258 let mut params = HashMap::new();
259 params.insert("steamID".to_string(), steam_id.to_string());
260
261 if let Some(ref nonce) = transfer.nonce {
262 params.insert("nonce".to_string(), nonce.clone());
263 }
264 if let Some(ref auth) = transfer.auth {
265 params.insert("auth".to_string(), auth.clone());
266 }
267
268 params
269}
270
271use std::time::Duration;
276
277use crate::http_client::{HttpClient, MultipartForm};
278
279#[derive(Debug)]
284pub enum TransferResult {
285 Success(Vec<String>),
287 Retry,
289 Error(SessionError),
291}
292
293pub async fn execute_single_transfer(http_client: &HttpClient, transfer: &TransferInfo, steam_id: u64) -> TransferResult {
295 let form = MultipartForm::new().text("steamID", steam_id.to_string());
297
298 let form = if let Some(ref nonce) = transfer.nonce { form.text("nonce", nonce.clone()) } else { form };
299
300 let form = if let Some(ref auth) = transfer.auth { form.text("auth", auth.clone()) } else { form };
301
302 let result = http_client.post_multipart(&transfer.url, form, HashMap::new()).await;
304
305 match result {
306 Ok(response) if response.is_success() => {
307 let cookies = if let Some(cookie_header) = response.get_header("set-cookie") {
309 extract_steam_login_cookies(cookie_header, &transfer.url)
310 } else {
311 let headers = response.get_all_headers("set-cookie");
313 let mut all_cookies = Vec::new();
314 for header in headers {
315 all_cookies.extend(extract_steam_login_cookies(header, &transfer.url));
316 }
317 all_cookies
318 };
319 TransferResult::Success(cookies)
320 }
321 Ok(response) => {
322 tracing::debug!("Transfer to {} returned status {}, will retry", transfer.url, response.status);
324 TransferResult::Retry
325 }
326 Err(e) => {
327 tracing::debug!("Transfer to {} failed with error: {:?}", transfer.url, e);
328 TransferResult::Retry
329 }
330 }
331}
332
333pub async fn execute_transfers_with_retry(http_client: &HttpClient, transfers: &[TransferInfo], steam_id: u64, max_retries: usize, retry_delay: Duration) -> Result<Vec<String>, SessionError> {
335 let mut all_cookies = Vec::new();
336
337 for transfer in transfers {
338 let mut last_status = None;
339
340 for attempt in 0..max_retries {
341 match execute_single_transfer(http_client, transfer, steam_id).await {
342 TransferResult::Success(cookies) => {
343 all_cookies.extend(cookies);
344 break;
345 }
346 TransferResult::Retry if attempt < max_retries - 1 => {
347 tokio::time::sleep(retry_delay).await;
349 }
350 TransferResult::Retry => {
351 last_status = Some("retry limit exceeded");
353 }
354 TransferResult::Error(e) => {
355 return Err(e);
356 }
357 }
358 }
359
360 if let Some(status) = last_status {
361 return Err(SessionError::NetworkError(format!("Transfer to {} failed: {} after {} attempts", transfer.url, status, max_retries)));
362 }
363 }
364
365 Ok(all_cookies)
366}
367
368#[cfg(test)]
369mod tests {
370 use super::*;
371
372 #[test]
373 fn test_extract_domain_from_url() {
374 assert_eq!(extract_domain_from_url("https://store.steampowered.com/login"), Some("store.steampowered.com".to_string()));
375 assert_eq!(extract_domain_from_url("https://steamcommunity.com/"), Some("steamcommunity.com".to_string()));
376 assert_eq!(extract_domain_from_url("invalid-url"), None);
377 }
378
379 #[test]
380 fn test_build_cookie_with_domain() {
381 let cookie = "steamLoginSecure=abc123";
382 let url = "https://store.steampowered.com/login";
383
384 let result = build_cookie_with_domain(cookie, url);
385 assert_eq!(result, Some("steamLoginSecure=abc123; Domain=store.steampowered.com".to_string()));
386 }
387
388 #[test]
389 fn test_build_cookie_with_existing_domain() {
390 let cookie = "steamLoginSecure=abc123; Domain=steampowered.com";
391 let url = "https://store.steampowered.com/login";
392
393 let result = build_cookie_with_domain(cookie, url);
394 assert_eq!(result, Some("steamLoginSecure=abc123; Domain=steampowered.com".to_string()));
395 }
396
397 #[test]
398 fn test_parse_cookies_from_header() {
399 let header = "steamLoginSecure=abc123, steamLoginSecure=def456";
400 let url = "https://store.steampowered.com/";
401
402 let cookies = parse_cookies_from_header(header, url);
403 assert_eq!(cookies.len(), 2);
404 assert!(cookies[0].contains("steamLoginSecure=abc123"));
405 assert!(cookies[0].contains("Domain=store.steampowered.com"));
406 }
407
408 #[test]
409 fn test_parse_cookies_ignores_non_login_cookies() {
410 let header = "sessionid=xyz, steamLoginSecure=abc123";
412 let url = "https://store.steampowered.com/";
413
414 let cookies = parse_cookies_from_header(header, url);
415 assert_eq!(cookies.len(), 1);
416 assert!(cookies[0].contains("steamLoginSecure=abc123"));
417 }
418
419 #[test]
420 fn test_extract_cookie_domains() {
421 let cookies = vec![
422 "steamLoginSecure=abc; Domain=store.steampowered.com".to_string(),
423 "steamLoginSecure=def; Domain=steamcommunity.com".to_string(),
424 "steamLoginSecure=ghi; Domain=login.steampowered.com".to_string(), ];
426
427 let domains = extract_cookie_domains(&cookies);
428 assert_eq!(domains.len(), 2);
429 assert!(domains.contains("store.steampowered.com"));
430 assert!(domains.contains("steamcommunity.com"));
431 assert!(!domains.contains("login.steampowered.com"));
432 }
433
434 #[test]
435 fn test_add_session_id_cookies() {
436 let mut cookies = Vec::new();
437 let mut domains = HashSet::new();
438 domains.insert("store.steampowered.com".to_string());
439 domains.insert("steamcommunity.com".to_string());
440
441 add_session_id_cookies(&mut cookies, "sess123", &domains);
442
443 assert_eq!(cookies.len(), 2);
444 for cookie in &cookies {
445 assert!(cookie.starts_with("sessionid=sess123"));
446 assert!(cookie.contains("Path=/"));
447 assert!(cookie.contains("Secure"));
448 }
449 }
450
451 #[test]
452 fn test_filter_session_id_cookies() {
453 let mut cookies = vec!["sessionid=old".to_string(), "steamLoginSecure=abc".to_string(), "sessionid=another".to_string()];
454
455 filter_session_id_cookies(&mut cookies);
456
457 assert_eq!(cookies.len(), 1);
458 assert_eq!(cookies[0], "steamLoginSecure=abc");
459 }
460
461 #[test]
462 fn test_build_simple_cookies() {
463 let cookies = build_simple_cookies(76561198000000000, "access_token_here", "sess123");
464
465 assert_eq!(cookies.len(), 2);
466 assert!(cookies[0].starts_with("steamLoginSecure="));
467 assert!(cookies[0].contains("76561198000000000"));
468 assert_eq!(cookies[1], "sessionid=sess123");
469 }
470
471 #[test]
472 fn test_parse_transfer_info() {
473 let json: serde_json::Value = serde_json::from_str(
474 r#"{
475 "transfer_info": [
476 {
477 "url": "https://store.steampowered.com/login/settoken",
478 "params": {
479 "nonce": "nonce123",
480 "auth": "auth456"
481 }
482 },
483 {
484 "url": "https://steamcommunity.com/login/settoken",
485 "params": {
486 "nonce": "nonce789"
487 }
488 }
489 ]
490 }"#,
491 )
492 .unwrap();
493
494 let result = parse_transfer_info(&json).unwrap();
495 assert_eq!(result.len(), 2);
496 assert_eq!(result[0].url, "https://store.steampowered.com/login/settoken");
497 assert_eq!(result[0].nonce, Some("nonce123".to_string()));
498 assert_eq!(result[0].auth, Some("auth456".to_string()));
499 assert_eq!(result[1].url, "https://steamcommunity.com/login/settoken");
500 assert!(result[1].auth.is_none());
501 }
502
503 #[test]
504 fn test_parse_transfer_info_missing() {
505 let json: serde_json::Value = serde_json::from_str("{}").unwrap();
506
507 let result = parse_transfer_info(&json);
508 assert!(result.is_err());
509 }
510
511 #[test]
512 fn test_check_finalize_error_no_error() {
513 let json: serde_json::Value = serde_json::from_str(
514 r#"{
515 "transfer_info": []
516 }"#,
517 )
518 .unwrap();
519
520 assert!(check_finalize_error(&json).is_ok());
521 }
522
523 #[test]
524 fn test_check_finalize_error_with_error() {
525 let json: serde_json::Value = serde_json::from_str(
526 r#"{
527 "error": "Invalid token"
528 }"#,
529 )
530 .unwrap();
531
532 let result = check_finalize_error(&json);
533 assert!(result.is_err());
534 }
535
536 #[test]
537 fn test_check_finalize_error_null_error() {
538 let json: serde_json::Value = serde_json::from_str(
539 r#"{
540 "error": null,
541 "transfer_info": []
542 }"#,
543 )
544 .unwrap();
545
546 assert!(check_finalize_error(&json).is_ok());
547 }
548
549 #[test]
550 fn test_extract_steam_login_cookies_single() {
551 let header = "steamLoginSecure=abc123; Path=/; HttpOnly";
552 let url = "https://store.steampowered.com/";
553
554 let cookies = extract_steam_login_cookies(header, url);
555 assert_eq!(cookies.len(), 1);
556 assert!(cookies[0].contains("steamLoginSecure=abc123"));
557 assert!(cookies[0].contains("Domain=store.steampowered.com"));
558 }
559
560 #[test]
561 fn test_extract_steam_login_cookies_multiple() {
562 let header = "steamLoginSecure=abc123, steamLoginSecure=def456";
563 let url = "https://store.steampowered.com/";
564
565 let cookies = extract_steam_login_cookies(header, url);
566 assert_eq!(cookies.len(), 2);
567 }
568
569 #[test]
570 fn test_extract_steam_login_cookies_with_newlines() {
571 let header = "other=cookie\nsteamLoginSecure=abc123";
572 let url = "https://store.steampowered.com/";
573
574 let cookies = extract_steam_login_cookies(header, url);
575 assert_eq!(cookies.len(), 1);
576 assert!(cookies[0].contains("steamLoginSecure=abc123"));
577 }
578
579 #[test]
580 fn test_extract_steam_login_cookies_ignores_other() {
581 let header = "sessionid=xyz; Path=/";
582 let url = "https://store.steampowered.com/";
583
584 let cookies = extract_steam_login_cookies(header, url);
585 assert!(cookies.is_empty());
586 }
587
588 #[test]
589 fn test_build_transfer_form_params_full() {
590 let transfer = TransferInfo { url: "https://test.com".to_string(), nonce: Some("nonce123".to_string()), auth: Some("auth456".to_string()) };
591
592 let params = build_transfer_form_params(&transfer, 76561198000000000);
593
594 assert_eq!(params.get("steamID"), Some(&"76561198000000000".to_string()));
595 assert_eq!(params.get("nonce"), Some(&"nonce123".to_string()));
596 assert_eq!(params.get("auth"), Some(&"auth456".to_string()));
597 }
598
599 #[test]
600 fn test_build_transfer_form_params_nonce_only() {
601 let transfer = TransferInfo { url: "https://test.com".to_string(), nonce: Some("nonce123".to_string()), auth: None };
602
603 let params = build_transfer_form_params(&transfer, 12345);
604
605 assert_eq!(params.get("steamID"), Some(&"12345".to_string()));
606 assert_eq!(params.get("nonce"), Some(&"nonce123".to_string()));
607 assert!(!params.contains_key("auth"));
608 }
609
610 #[test]
611 fn test_build_transfer_form_params_minimal() {
612 let transfer = TransferInfo { url: "https://test.com".to_string(), nonce: None, auth: None };
613
614 let params = build_transfer_form_params(&transfer, 99999);
615
616 assert_eq!(params.len(), 1);
617 assert_eq!(params.get("steamID"), Some(&"99999".to_string()));
618 }
619}