mirror of
https://github.com/alterware/alterware-launcher.git
synced 2025-12-04 07:17:50 +00:00
backport cdn rating
This commit is contained in:
9
Cargo.lock
generated
9
Cargo.lock
generated
@@ -23,6 +23,7 @@ version = "0.10.5"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"blake3",
|
"blake3",
|
||||||
"colored",
|
"colored",
|
||||||
|
"futures",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"indicatif",
|
"indicatif",
|
||||||
"mslnk",
|
"mslnk",
|
||||||
@@ -410,9 +411,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures"
|
name = "futures"
|
||||||
version = "0.3.30"
|
version = "0.3.31"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0"
|
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
@@ -441,9 +442,9 @@ checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-executor"
|
name = "futures-executor"
|
||||||
version = "0.3.30"
|
version = "0.3.31"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d"
|
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-task",
|
"futures-task",
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ colored = "3.0.0"
|
|||||||
once_cell = "1.20.3"
|
once_cell = "1.20.3"
|
||||||
reqwest = { version = "0.12.12", features = ["stream"] }
|
reqwest = { version = "0.12.12", features = ["stream"] }
|
||||||
futures-util = "0.3.31"
|
futures-util = "0.3.31"
|
||||||
|
futures = "0.3.31"
|
||||||
indicatif = "0.17.11"
|
indicatif = "0.17.11"
|
||||||
tokio = { version="1.43.0", features = ["rt-multi-thread", "macros"] }
|
tokio = { version="1.43.0", features = ["rt-multi-thread", "macros"] }
|
||||||
simple-log = "2.3.0"
|
simple-log = "2.3.0"
|
||||||
|
|||||||
161
src/cdn.rs
Normal file
161
src/cdn.rs
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
use crate::global::CDN_HOSTS;
|
||||||
|
use crate::http;
|
||||||
|
use futures::future::join_all;
|
||||||
|
use simple_log::*;
|
||||||
|
use std::sync::{Arc, Mutex, RwLock};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
static CURRENT_CDN: Mutex<Option<Arc<Server>>> = Mutex::new(None);
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub enum Region {
|
||||||
|
Europe,
|
||||||
|
Global,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct Server {
|
||||||
|
pub host: &'static str,
|
||||||
|
pub rating: u8,
|
||||||
|
pub latency: Option<std::time::Duration>,
|
||||||
|
pub region: Region,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Server {
|
||||||
|
pub const fn new(host: &'static str, region: Region) -> Self {
|
||||||
|
Server {
|
||||||
|
host,
|
||||||
|
rating: 255,
|
||||||
|
latency: None,
|
||||||
|
region,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn url(&self) -> String {
|
||||||
|
format!("https://{}/", self.host)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn rate(&mut self, asn: u32, is_initial: bool) {
|
||||||
|
let timeout = if is_initial {
|
||||||
|
Duration::from_millis(500)
|
||||||
|
} else {
|
||||||
|
Duration::from_millis(5000)
|
||||||
|
};
|
||||||
|
match http::rating_request(&self.url(), timeout).await {
|
||||||
|
Ok((latency, is_cloudflare)) => {
|
||||||
|
self.latency = Some(latency);
|
||||||
|
// Always use complete rating calculation with all available information
|
||||||
|
self.rating = self.calculate_rating(latency, is_cloudflare, asn);
|
||||||
|
|
||||||
|
info!(
|
||||||
|
"Server {} rated {} ({}ms, rating: {}, cloudflare: {}, region: {:?})",
|
||||||
|
self.host,
|
||||||
|
self.rating,
|
||||||
|
latency.as_millis(),
|
||||||
|
self.rating,
|
||||||
|
is_cloudflare,
|
||||||
|
self.region
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to connect to {}: {}", self.host, e);
|
||||||
|
self.rating = 0;
|
||||||
|
self.latency = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn calculate_initial_rating(&self, latency: std::time::Duration) -> u8 {
|
||||||
|
let mut rating: f32 = 255.0;
|
||||||
|
let ms = latency.as_millis() as f32;
|
||||||
|
let latency_mult = (200.0 / ms.max(200.0)).powf(0.5);
|
||||||
|
rating *= latency_mult;
|
||||||
|
rating.clamp(1.0, 255.0) as u8
|
||||||
|
}
|
||||||
|
|
||||||
|
fn calculate_rating(&self, latency: std::time::Duration, is_cloudflare: bool, asn: u32) -> u8 {
|
||||||
|
let mut rating = self.calculate_initial_rating(latency);
|
||||||
|
|
||||||
|
// Additional factors for full rating
|
||||||
|
if is_cloudflare {
|
||||||
|
// 3320/DTAG: bad cf peering
|
||||||
|
// 5483/Magyar Telekom: sub. of DTAG
|
||||||
|
if asn == 3320 || asn == 5483 {
|
||||||
|
rating = (rating as f32 * 0.1) as u8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.region == Region::Global {
|
||||||
|
rating = (rating as f32 * 1.1).min(255.0) as u8;
|
||||||
|
}
|
||||||
|
|
||||||
|
rating
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Hosts {
|
||||||
|
pub servers: Vec<Server>,
|
||||||
|
pub active_index: RwLock<Option<usize>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hosts {
|
||||||
|
/// create new rated hosts instance
|
||||||
|
pub async fn new() -> Self {
|
||||||
|
let mut hosts = Hosts {
|
||||||
|
servers: CDN_HOSTS.to_vec(),
|
||||||
|
active_index: RwLock::new(None),
|
||||||
|
};
|
||||||
|
|
||||||
|
let asn = crate::http::get_asn().await;
|
||||||
|
hosts.rate(asn, true).await;
|
||||||
|
hosts
|
||||||
|
}
|
||||||
|
|
||||||
|
/// get the URL of the currently active CDN
|
||||||
|
pub fn active_url(&self) -> Option<String> {
|
||||||
|
CURRENT_CDN.lock().unwrap().as_ref().map(|s| s.url())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// set the next best host based on ratings
|
||||||
|
pub fn next(&self) -> bool {
|
||||||
|
if self.servers.is_empty() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// find best host by rating, fifo if equal
|
||||||
|
if let Some((idx, _)) = self
|
||||||
|
.servers
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.max_by_key(|(idx, server)| (server.rating, -(*idx as i32)))
|
||||||
|
{
|
||||||
|
let server = &self.servers[idx];
|
||||||
|
*CURRENT_CDN.lock().unwrap() = Some(Arc::new(*server));
|
||||||
|
*self.active_index.write().unwrap() = Some(idx);
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// rate and order all servers, then select the best one
|
||||||
|
pub async fn rate(&mut self, asn: u32, is_initial: bool) {
|
||||||
|
let rating_futures: Vec<_> = self
|
||||||
|
.servers
|
||||||
|
.iter_mut()
|
||||||
|
.map(|server| server.rate(asn, is_initial))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
join_all(rating_futures).await;
|
||||||
|
|
||||||
|
// reset state and select best host
|
||||||
|
*self.active_index.write().unwrap() = None;
|
||||||
|
*CURRENT_CDN.lock().unwrap() = None;
|
||||||
|
self.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the best CDN URL for use
|
||||||
|
pub fn get_master_url(&self) -> Option<String> {
|
||||||
|
self.active_url()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,19 +1,35 @@
|
|||||||
use crate::http_async;
|
|
||||||
use crate::structs::PrintPrefix;
|
use crate::structs::PrintPrefix;
|
||||||
use colored::Colorize;
|
use colored::Colorize;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use serde_json::Value;
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
use crate::cdn::{Hosts, Region, Server};
|
||||||
|
|
||||||
pub const GH_OWNER: &str = "mxve";
|
pub const GH_OWNER: &str = "mxve";
|
||||||
pub const GH_REPO: &str = "alterware-launcher";
|
pub const GH_REPO: &str = "alterware-launcher";
|
||||||
pub const GH_IW4X_OWNER: &str = "iw4x";
|
pub const GH_IW4X_OWNER: &str = "iw4x";
|
||||||
pub const GH_IW4X_REPO: &str = "iw4x-client";
|
pub const GH_IW4X_REPO: &str = "iw4x-client";
|
||||||
pub const DEFAULT_MASTER: &str = "https://cdn.alterware.ovh";
|
pub const DEFAULT_MASTER: &str = "https://cdn.alterware.ovh";
|
||||||
pub const BACKUP_MASTER: &str = "https://cdn.getserve.rs";
|
|
||||||
|
pub const CDN_HOSTS: [Server; 2] = [
|
||||||
|
Server::new("cdn.alterware.ovh", Region::Global),
|
||||||
|
Server::new("cdn.iw4x.dev", Region::Europe),
|
||||||
|
];
|
||||||
|
|
||||||
|
pub const IP2ASN: &str = "https://ip2asn.getserve.rs/v1/as/ip/self";
|
||||||
|
|
||||||
|
pub static USER_AGENT: Lazy<String> = Lazy::new(|| {
|
||||||
|
format!(
|
||||||
|
"AlterWare Launcher v{} on {} | github.com/{}/{}",
|
||||||
|
env!("CARGO_PKG_VERSION"),
|
||||||
|
std::env::consts::OS,
|
||||||
|
GH_OWNER,
|
||||||
|
GH_REPO
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
pub static MASTER_URL: Lazy<Mutex<String>> = Lazy::new(|| Mutex::new(String::from(DEFAULT_MASTER)));
|
pub static MASTER_URL: Lazy<Mutex<String>> = Lazy::new(|| Mutex::new(String::from(DEFAULT_MASTER)));
|
||||||
|
|
||||||
@@ -66,58 +82,53 @@ pub static PREFIXES: Lazy<HashMap<&'static str, PrintPrefix>> = Lazy::new(|| {
|
|||||||
])
|
])
|
||||||
});
|
});
|
||||||
|
|
||||||
|
pub async fn check_connectivity_and_rate_cdns() -> Pin<Box<dyn Future<Output = bool> + Send>> {
|
||||||
|
Box::pin(async move {
|
||||||
|
crate::println_info!("Initializing CDN rating system...");
|
||||||
|
let hosts = Hosts::new().await;
|
||||||
|
let best_cdn = hosts.get_master_url();
|
||||||
|
|
||||||
|
if let Some(cdn_url) = best_cdn {
|
||||||
|
let cdn_url = cdn_url.trim_end_matches('/');
|
||||||
|
*MASTER_URL.lock().unwrap() = cdn_url.to_string();
|
||||||
|
crate::println_info!("Selected CDN: {}", cdn_url);
|
||||||
|
|
||||||
|
match crate::http_async::get_body_string(cdn_url).await {
|
||||||
|
Ok(_) => {
|
||||||
|
info!("Successfully connected to CDN: {}", cdn_url);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to connect to selected CDN {}: {}", cdn_url, e);
|
||||||
|
*IS_OFFLINE.lock().unwrap() = true;
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
crate::println_error!("No CDN hosts are available");
|
||||||
|
*IS_OFFLINE.lock().unwrap() = true;
|
||||||
|
false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn check_connectivity(
|
pub fn check_connectivity(
|
||||||
master_url: Option<String>,
|
master_url: Option<String>,
|
||||||
) -> Pin<Box<dyn Future<Output = bool> + Send>> {
|
) -> Pin<Box<dyn Future<Output = bool> + Send>> {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let retry = master_url.is_some();
|
if let Some(url) = master_url {
|
||||||
if !retry {
|
*MASTER_URL.lock().unwrap() = url.clone();
|
||||||
crate::println_info!("Running connectivity check on {}", DEFAULT_MASTER);
|
crate::println_info!("Using fallback connectivity check on {}", url);
|
||||||
} 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();
|
match crate::http_async::get_body_string(&url).await {
|
||||||
|
|
||||||
// Check ASN number using the new get_json function
|
|
||||||
let asn_response: Result<Value, String> =
|
|
||||||
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 || as_number == 5483) && master_url == DEFAULT_MASTER {
|
|
||||||
*MASTER_URL.lock().unwrap() = String::from(BACKUP_MASTER);
|
|
||||||
crate::println_info!(
|
|
||||||
"Detected Deutsche Telekom/DTAG/Magyar Telekom as ISP, switching 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,
|
Ok(_) => true,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
*IS_OFFLINE.lock().unwrap() = true;
|
*IS_OFFLINE.lock().unwrap() = true;
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
if !result {
|
|
||||||
crate::println_error!("Failed to connect to CDN {}", master_url);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we switched to backup, do not retry
|
|
||||||
if !retry && !result && !switched_to_backup {
|
|
||||||
check_connectivity(Some(String::from(BACKUP_MASTER))).await
|
|
||||||
} else {
|
} else {
|
||||||
result
|
check_connectivity_and_rate_cdns().await.await
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
102
src/http.rs
Normal file
102
src/http.rs
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
use reqwest::header::HeaderMap;
|
||||||
|
use serde_json::Value;
|
||||||
|
use simple_log::*;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
/// Wrapper to make a quick request and get body
|
||||||
|
pub async fn quick_request(url: &str) -> Result<String, Box<dyn std::error::Error>> {
|
||||||
|
info!("Making a quick request to: {}", url);
|
||||||
|
let client = reqwest::Client::builder()
|
||||||
|
.timeout(Duration::from_secs(5))
|
||||||
|
.build()?;
|
||||||
|
|
||||||
|
let res = client
|
||||||
|
.get(url)
|
||||||
|
.header("User-Agent", crate::global::USER_AGENT.to_string())
|
||||||
|
.send()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if let Err(e) = &res {
|
||||||
|
error!("Failed to get {}: {}", url, e);
|
||||||
|
return Err(format!("Failed to get {} {}", url, e).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = res.unwrap();
|
||||||
|
match res.text().await {
|
||||||
|
Ok(text) => {
|
||||||
|
info!("Successfully received response from: {}", url);
|
||||||
|
Ok(text)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Failed to get response text from {}: {}", url, e);
|
||||||
|
Err(e.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if server is using Cloudflare based on headers
|
||||||
|
fn is_cloudflare(headers: &HeaderMap) -> bool {
|
||||||
|
headers.contains_key("cf-ray")
|
||||||
|
|| headers.contains_key("cf-cache-status")
|
||||||
|
|| headers
|
||||||
|
.get("server")
|
||||||
|
.is_some_and(|v| v.as_bytes().starts_with(b"cloudflare"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Make a request for rating purposes, measuring latency and detecting Cloudflare
|
||||||
|
pub async fn rating_request(
|
||||||
|
url: &str,
|
||||||
|
timeout: Duration,
|
||||||
|
) -> Result<(Duration, bool), Box<dyn std::error::Error>> {
|
||||||
|
info!(
|
||||||
|
"Making a rating request to: {} with timeout {:?}",
|
||||||
|
url, timeout
|
||||||
|
);
|
||||||
|
let client = reqwest::Client::builder().timeout(timeout).build()?;
|
||||||
|
|
||||||
|
let start = Instant::now();
|
||||||
|
let res = client
|
||||||
|
.get(url)
|
||||||
|
.header("User-Agent", crate::global::USER_AGENT.to_string())
|
||||||
|
.send()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let latency = start.elapsed();
|
||||||
|
|
||||||
|
if let Err(e) = &res {
|
||||||
|
error!("Failed to get {}: {} (after {:?})", url, e, latency);
|
||||||
|
return Err(format!("Failed to get {} {} (after {:?})", url, e, latency).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = res.unwrap();
|
||||||
|
let headers = res.headers().clone();
|
||||||
|
let is_cloudflare = is_cloudflare(&headers);
|
||||||
|
|
||||||
|
// We don't need the response body for rating
|
||||||
|
if let Err(e) = res.text().await {
|
||||||
|
warn!(
|
||||||
|
"Failed to get response text from {}: {} (after {:?})",
|
||||||
|
url, e, latency
|
||||||
|
);
|
||||||
|
return Err(e.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
info!(
|
||||||
|
"Successfully rated {} in {:?} (cloudflare: {})",
|
||||||
|
url, latency, is_cloudflare
|
||||||
|
);
|
||||||
|
Ok((latency, is_cloudflare))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieve client ASN
|
||||||
|
pub async fn get_asn() -> u32 {
|
||||||
|
let response = quick_request(crate::global::IP2ASN).await;
|
||||||
|
if let Ok(as_data_str) = response {
|
||||||
|
if let Ok(as_data) = serde_json::from_str::<Value>(&as_data_str) {
|
||||||
|
if let Some(as_number) = as_data.get("as_number").and_then(|v| v.as_u64()) {
|
||||||
|
return as_number as u32;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
0
|
||||||
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
mod cache;
|
mod cache;
|
||||||
|
mod cdn;
|
||||||
mod config;
|
mod config;
|
||||||
mod extend;
|
mod extend;
|
||||||
mod github;
|
mod github;
|
||||||
mod global;
|
mod global;
|
||||||
|
mod http;
|
||||||
mod http_async;
|
mod http_async;
|
||||||
mod iw4x;
|
mod iw4x;
|
||||||
mod misc;
|
mod misc;
|
||||||
@@ -726,7 +728,11 @@ async fn main() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if !cfg.offline && !cfg.skip_connectivity_check {
|
if !cfg.offline && !cfg.skip_connectivity_check {
|
||||||
|
if initial_cdn.is_some() {
|
||||||
cfg.offline = !global::check_connectivity(initial_cdn).await;
|
cfg.offline = !global::check_connectivity(initial_cdn).await;
|
||||||
|
} else {
|
||||||
|
cfg.offline = !global::check_connectivity_and_rate_cdns().await.await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.offline {
|
if cfg.offline {
|
||||||
|
|||||||
Reference in New Issue
Block a user