1use std::env;
2use std::path::Path;
3use std::time::Duration;
4use once_cell::sync::Lazy;
5use reqwest::Client;
6use thiserror::Error;
7
8pub static HTTP_CLIENT: Lazy<Client> = Lazy::new(|| {
10 Client::builder()
11 .pool_max_idle_per_host(100)
12 .pool_idle_timeout(Some(Duration::from_secs(90)))
13
14 .http2_initial_stream_window_size(Some(2 * 1024 * 1024))
15 .http2_initial_connection_window_size(Some(4 * 1024 * 1024))
16 .http2_adaptive_window(true)
17 .http2_max_frame_size(Some(16 * 1024))
18
19 .tcp_keepalive(Some(Duration::from_secs(60)))
20 .tcp_nodelay(true)
21
22 .timeout(Duration::from_secs(60))
23 .connect_timeout(Duration::from_secs(5))
24
25 .zstd(true)
26 .gzip(true)
27 .brotli(true)
28
29 .build()
30 .expect("Failed to build HTTP client with default configuration - this should never fail")
31});
32
33
34#[cfg(target_os = "windows")]
35const HOSTS_PATH: &str = "System32\\drivers\\etc\\hosts";
36
37#[cfg(not(target_os = "windows"))]
38const HOSTS_PATH: &str = "etc/hosts";
39
40const HOSTS: [&str; 5] = [
44 "mojang.com",
45 "minecraft.net",
46 "minecraftservices.com",
47 "microsoftonline.com",
48 "xboxlive.com",
49];
50
51#[derive(Debug, Error)]
53pub enum HostsError {
54 #[error("Failed to read hosts file at {0}")]
55 HostsReadError(String),
56
57 #[error("I/O error: {0}")]
58 IoError(#[from] std::io::Error),
59}
60
61pub type HostsResult<T> = std::result::Result<T, HostsError>;
62
63pub fn blocked_launcher_domains(extra: &[&str]) -> HostsResult<Vec<String>> {
69 let hosts_path = if cfg!(target_os = "windows") {
70 let system_drive = env::var("SystemDrive").unwrap_or("C:".to_string());
71 format!("{}\\{}", system_drive, HOSTS_PATH)
72 } else {
73 format!("/{}", HOSTS_PATH)
74 };
75
76 if !Path::new(&hosts_path).exists() {
77 return Ok(Vec::new());
78 }
79
80 let hosts_file = std::fs::read_to_string(&hosts_path)
81 .map_err(|_| HostsError::HostsReadError(hosts_path.clone()))?;
82
83 Ok(hosts_file
84 .lines()
85 .filter(|line| !line.trim_start().starts_with('#'))
86 .flat_map(|line| line.split_whitespace().skip(1))
87 .filter(|host| is_watched_host(host, extra))
88 .map(str::to_string)
89 .collect())
90}
91
92fn is_watched_host(host: &str, extra: &[&str]) -> bool {
95 let host = host.trim_end_matches('.').to_ascii_lowercase();
96
97 HOSTS.iter().chain(extra).any(|domain| {
98 host == *domain
99 || host
100 .strip_suffix(*domain)
101 .is_some_and(|prefix| prefix.ends_with('.'))
102 })
103}