diff --git a/Cargo.lock b/Cargo.lock index daecfc4..dd1feff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,7 +19,7 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "alterware-launcher" -version = "0.9.3" +version = "0.10.2" dependencies = [ "blake3", "colored", diff --git a/Cargo.toml b/Cargo.toml index a328130..c7fd8ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "alterware-launcher" -version = "0.9.3" +version = "0.10.2" edition = "2021" build = "res/build.rs" diff --git a/README.md b/README.md index 6b08449..427ea3e 100644 --- a/README.md +++ b/README.md @@ -150,6 +150,9 @@ - Install or reinstall redistributables - ```--prerelease``` - Update to prerelease version of clients (currently only available for IW4x) and launcher +- ```--cdn-url``` +- ```--offline``` +- ```--skip-connectivity-check``` ##### Example: ```shell @@ -171,6 +174,9 @@ alterware-launcher.exe iw4x --bonus -u --path "C:\Games\IW4x" --pass "-console" - `use_https`: Use HTTPS for downloads. Default: `true`. - `skip_redist`: Skip redistributable installations. Default: `false`. - `prerelease`: Update to prerelease version of clients and launcher. Default: `false`. +- `cdn_url` +- `offline` +- `skip-connectivity-check` --- diff --git a/src/global.rs b/src/global.rs index ca5cef5..a1d46cc 100644 --- a/src/global.rs +++ b/src/global.rs @@ -3,14 +3,21 @@ use colored::Colorize; use once_cell::sync::Lazy; use std::collections::HashMap; use std::sync::Mutex; +use std::pin::Pin; +use std::future::Future; +use crate::http_async; +use serde_json::Value; pub const GH_OWNER: &str = "mxve"; pub const GH_REPO: &str = "alterware-launcher"; pub const GH_IW4X_OWNER: &str = "iw4x"; pub const GH_IW4X_REPO: &str = "iw4x-client"; +pub const DEFAULT_MASTER: &str = "https://cdn.alterware.ovh"; +pub const BACKUP_MASTER: &str = "https://cdn.iw4x.getserve.rs"; -pub static MASTER: Lazy> = - Lazy::new(|| Mutex::new("https://cdn.alterware.ovh".to_owned())); +pub static MASTER_URL: Lazy> = Lazy::new(|| { + Mutex::new(String::from(DEFAULT_MASTER)) +}); pub static IS_OFFLINE: Lazy> = Lazy::new(|| Mutex::new(false)); @@ -54,14 +61,52 @@ pub static PREFIXES: Lazy> = Lazy::new(|| { ]) }); -pub async fn check_connectivity() -> bool { - let master_url = MASTER.lock().unwrap().clone(); +pub fn check_connectivity(master_url: Option) -> Pin + Send>> { + Box::pin(async move { + let retry = master_url.is_some(); + if !retry { + crate::println_info!("Running connectivity check on {}", DEFAULT_MASTER); + } else { + let master = master_url.unwrap(); + *MASTER_URL.lock().unwrap() = master.clone(); + crate::println_info!("Running connectivity check on {}", master); + } + + let master_url = MASTER_URL.lock().unwrap().clone(); + + // Check ASN number using the new get_json function + let asn_response: Result = http_async::get_json("https://ip2asn.getserve.rs/v1/as/ip/self").await; + + let mut switched_to_backup = false; + + if let Ok(asn_data) = asn_response { + if let Some(as_number) = asn_data.get("as_number").and_then(|v| v.as_i64()) { + if as_number == 3320 && master_url == DEFAULT_MASTER { + *MASTER_URL.lock().unwrap() = String::from(BACKUP_MASTER); + crate::println_info!("Detected DTAG as ISP, switched to backup master URL: {}", BACKUP_MASTER); + switched_to_backup = true; + } + } + } + + // Run connectivity check regardless of ASN switch + let result = match crate::http_async::get_body_string(&master_url).await { + Ok(_) => true, + Err(_) => { + *IS_OFFLINE.lock().unwrap() = true; + false + } + }; + + if !result { + crate::println_error!("Failed to connect to CDN {}", master_url); + } - match crate::http_async::get_body_string(&master_url).await { - Ok(_) => true, - Err(_) => { - *IS_OFFLINE.lock().unwrap() = true; - false + // If we switched to backup, do not retry + if !retry && !result && !switched_to_backup { + check_connectivity(Some(String::from(BACKUP_MASTER))).await + } else { + result } - } + }) } diff --git a/src/http_async.rs b/src/http_async.rs index 2971b4a..fd10ce1 100644 --- a/src/http_async.rs +++ b/src/http_async.rs @@ -103,3 +103,34 @@ pub async fn get_body_string(url: &str) -> Result { let body = get_body(url).await?; Ok(String::from_utf8(body).unwrap()) } + +pub async fn get_json(url: &str) -> Result { + let client = Client::new(); + let res = client + .get(url) + .header( + "User-Agent", + format!( + "AlterWare Launcher | github.com/{}/{}", + crate::global::GH_OWNER, + crate::global::GH_REPO + ), + ) + .header("Accept", "application/json") + .send() + .await + .map_err(|e| format!("Failed to send request: {}", e))?; + + debug!("{} {}", res.status(), url); + + if !res.status().is_success() { + return Err(format!("Request failed with status: {}", res.status())); + } + + let body = res.bytes() + .await + .map_err(|e| format!("Failed to read response body: {}", e))?; + + serde_json::from_slice::(&body) + .map_err(|e| format!("Failed to parse JSON: {}", e)) +} diff --git a/src/main.rs b/src/main.rs index 0ee3b88..6b9f181 100644 --- a/src/main.rs +++ b/src/main.rs @@ -272,7 +272,7 @@ async fn update_dir( let mut bust_cache = false; let mut local_hash = String::default(); while !download_complete { - let url = format!("{}/{}", MASTER.lock().unwrap(), file.name); + let url = format!("{}/{}", MASTER_URL.lock().unwrap(), file.name); let url = if bust_cache { bust_cache = false; format!("{}?{}", url, misc::random_string(6)) @@ -347,7 +347,7 @@ async fn update( let ignore_required_files = ignore_required_files.unwrap_or(false); let res = - http_async::get_body_string(format!("{}/files.json", MASTER.lock().unwrap()).as_str()) + http_async::get_body_string(format!("{}/files.json", MASTER_URL.lock().unwrap()).as_str()) .await .unwrap(); debug!("Retrieved files.json from server"); @@ -653,6 +653,8 @@ async fn main() { println!(" --skip-redist: Skip redistributable installation"); println!(" --redist: (Re-)Install redistributables"); println!(" --prerelease: Update to prerelease version of clients and launcher"); + println!(" --offline: Run in offline mode"); + println!(" --skip-connectivity-check: Don't check connectivity"); println!( "\nExample:\n alterware-launcher.exe iw4x --bonus --pass \"-console -nointro\"" ); @@ -679,8 +681,46 @@ async fn main() { return; } - let offline_mode = !global::check_connectivity().await; - if offline_mode { + let install_path: PathBuf; + if let Some(path) = arg_value(&args, "--path") { + install_path = PathBuf::from(path); + arg_remove_value(&mut args, "--path"); + } else if let Some(path) = arg_value(&args, "-p") { + install_path = PathBuf::from(path); + arg_remove_value(&mut args, "-p"); + } else { + install_path = env::current_dir().unwrap(); + } + + let mut cfg = config::load(install_path.join("alterware-launcher.json")); + + if let Some(cdn_url) = arg_value(&args, "--cdn-url") { + cfg.cdn_url = cdn_url; + arg_remove_value(&mut args, "--cdn-url"); + } + + if arg_bool(&args, "--offline") { + cfg.offline = true; + arg_remove(&mut args, "--offline"); + } + + if arg_bool(&args, "--skip-connectivity-check") { + cfg.skip_connectivity_check = true; + arg_remove(&mut args, "--skip-connectivity-check"); + } + + let initial_cdn = if !cfg.cdn_url.is_empty() { + info!("Using custom CDN URL: {}", cfg.cdn_url); + Some(cfg.cdn_url.clone()) + } else { + None + }; + + if !cfg.offline && !cfg.skip_connectivity_check { + cfg.offline = !global::check_connectivity(initial_cdn).await; + } + + if cfg.offline { // Check if this is a first-time run (no stored data) let stored_data = cache::get_stored_data(); if stored_data.is_none() { @@ -701,20 +741,6 @@ async fn main() { ); warn!("No internet connection or MASTER server is unreachable. Running in offline mode..."); - // Handle path the same way as online mode - let install_path: PathBuf; - if let Some(path) = arg_value(&args, "--path") { - install_path = PathBuf::from(path); - arg_remove_value(&mut args, "--path"); - } else if let Some(path) = arg_value(&args, "-p") { - install_path = PathBuf::from(path); - arg_remove_value(&mut args, "-p"); - } else { - install_path = env::current_dir().unwrap(); - } - - let cfg = config::load(install_path.join("alterware-launcher.json")); - // Try to get stored game data let stored_data = cache::get_stored_data(); if let Some(ref data) = stored_data { @@ -763,21 +789,8 @@ async fn main() { return; } - let install_path: PathBuf; - if let Some(path) = arg_value(&args, "--path") { - install_path = PathBuf::from(path); - arg_remove_value(&mut args, "--path"); - } else if let Some(path) = arg_value(&args, "-p") { - install_path = PathBuf::from(path); - arg_remove_value(&mut args, "-p"); - } else { - install_path = env::current_dir().unwrap(); - } - - let mut cfg = config::load(install_path.join("alterware-launcher.json")); - if !cfg.use_https { - let mut master_url = MASTER.lock().unwrap(); + let mut master_url = MASTER_URL.lock().unwrap(); *master_url = master_url.replace("https://", "http://"); }; @@ -841,7 +854,7 @@ async fn main() { } let games_json = - http_async::get_body_string(format!("{}/games.json", MASTER.lock().unwrap()).as_str()) + http_async::get_body_string(format!("{}/games.json", MASTER_URL.lock().unwrap()).as_str()) .await .unwrap_or_else(|error| { crate::println_error!("Failed to get games.json: {:#?}", error); diff --git a/src/structs.rs b/src/structs.rs index e326d28..e9e02a6 100644 --- a/src/structs.rs +++ b/src/structs.rs @@ -48,6 +48,12 @@ pub struct Config { pub skip_redist: bool, #[serde(default)] pub prerelease: bool, + #[serde(default)] + pub cdn_url: String, + #[serde(default)] + pub offline: bool, + #[serde(default)] + pub skip_connectivity_check: bool, } impl Default for Config { @@ -63,6 +69,9 @@ impl Default for Config { use_https: true, skip_redist: false, prerelease: false, + cdn_url: String::default(), + offline: false, + skip_connectivity_check: false, } } }