Skip to main content

lighty_core/
hosts.rs

1use std::env;
2use std::path::Path;
3use std::time::Duration;
4use once_cell::sync::Lazy;
5use reqwest::Client;
6use thiserror::Error;
7
8/// Shared HTTP client tuned for parallel asset/library downloads.
9pub 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
40/// Hosts the launcher must be able to reach. Antivirus, parental filters and
41/// cracked-launcher installers routinely blackhole these in the hosts file,
42/// which breaks login; add a domain here when a launch depends on reaching it.
43const HOSTS: [&str; 5] = [
44    "mojang.com",
45    "minecraft.net",
46    "minecraftservices.com",
47    "microsoftonline.com",
48    "xboxlive.com",
49];
50
51/// Errors related to the hosts file check.
52#[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
63/// Returns the hosts entries intercepting a domain the launcher needs; empty
64/// means the file is clean. `extra` adds a launcher's own auth domain.
65///
66/// Synchronous on purpose: the file is tiny and [`crate::AppState::init`],
67/// which calls it, is not async.
68pub 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
92/// An entry matches when it is the domain itself or one of its subdomains —
93/// a `contains` test also fired on lookalikes such as mojang.com.evil.tld.
94fn 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}