Skip to main content

lighty_core/
hosts.rs

1use std::env;
2use std::time::Duration;
3use once_cell::sync::Lazy;
4use reqwest::Client;
5use tokio::fs;
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/// Auth-critical domains we refuse to see redirected by a hijacked hosts file.
41const HOSTS: [&str; 3] = [
42    "mojang.com",
43    "minecraft.net",
44    "lightylauncher.fr",
45];
46
47/// Errors related to the hosts file check.
48#[derive(Debug, Error)]
49pub enum HostsError {
50    #[error("Failed to read hosts file at {0}")]
51    HostsReadError(String),
52
53    #[error("Hosts file contains blocked entries: {0}")]
54    HostsBlocked(String),
55
56    #[error("I/O error: {0}")]
57    IoError(#[from] std::io::Error),
58}
59
60pub type HostsResult<T> = std::result::Result<T, HostsError>;
61
62/// Checks whether the system hosts file redirects any auth-critical domains.
63pub async fn check_hosts_file() -> HostsResult<()> {
64    let hosts_path = if cfg!(target_os = "windows") {
65        let system_drive = env::var("SystemDrive").unwrap_or("C:".to_string());
66        format!("{}\\{}", system_drive, HOSTS_PATH)
67    } else {
68        format!("/{}", HOSTS_PATH)
69    };
70
71    if !fs::try_exists(&hosts_path).await? {
72        return Ok(());
73    }
74
75    let hosts_file = fs::read_to_string(&hosts_path)
76        .await
77        .map_err(|_| HostsError::HostsReadError(hosts_path.clone()))?;
78
79    let flagged_entries: Vec<_> = hosts_file
80        .lines()
81        .filter(|line| !line.trim_start().starts_with('#'))
82        .flat_map(|line| {
83            let mut parts = line.split_whitespace();
84            let _ip = parts.next();
85            parts.filter(|domain| HOSTS.iter().any(|&entry| domain.contains(entry)))
86        })
87        .map(|s| s.to_string())
88        .collect();
89
90    if !flagged_entries.is_empty() {
91        return Err(HostsError::HostsBlocked(flagged_entries.join("\n")));
92    }
93
94    Ok(())
95}