Make mrpack downloads HTTPS-only (#5882)

* Add set of trusted download hosts for mrpacks

* split secure/insecure reqwest client

* make fetching https-only

* lint fix
This commit is contained in:
aecsocket
2026-04-23 20:04:38 +01:00
committed by GitHub
parent 6862cf5ab2
commit 11ac27f71f
6 changed files with 89 additions and 30 deletions

View File

@@ -4,11 +4,11 @@ use reqwest::StatusCode;
use crate::State;
use crate::state::{Credentials, MinecraftLoginFlow};
use crate::util::fetch::REQWEST_CLIENT;
use crate::util::fetch::INSECURE_REQWEST_CLIENT;
#[tracing::instrument]
pub async fn check_reachable() -> crate::Result<()> {
let resp = REQWEST_CLIENT
let resp = INSECURE_REQWEST_CLIENT
.get("https://sessionserver.mojang.com/session/minecraft/hasJoined")
.send()
.await?;

View File

@@ -14,7 +14,7 @@ use tokio_util::compat::FuturesAsyncReadCompatExt;
use url::Url;
use crate::{
ErrorKind, minecraft_skins::UrlOrBlob, util::fetch::REQWEST_CLIENT,
ErrorKind, minecraft_skins::UrlOrBlob, util::fetch::INSECURE_REQWEST_CLIENT,
};
pub async fn url_to_data_stream(
@@ -25,7 +25,7 @@ pub async fn url_to_data_stream(
Ok(Either::Left(stream::once(async { Ok(data) })))
} else {
let response = REQWEST_CLIENT
let response = INSECURE_REQWEST_CLIENT
.get(url.as_str())
.header("Accept", "image/png")
.send()

View File

@@ -863,7 +863,7 @@ async fn run_credentials(
if !project_id.trim().is_empty() {
let server_id = uuid::Uuid::new_v4().to_string();
let join_result = fetch::REQWEST_CLIENT
let join_result = fetch::INSECURE_REQWEST_CLIENT
.post("https://sessionserver.mojang.com/session/minecraft/join")
.json(&json!({
"accessToken": &credentials.access_token,

View File

@@ -1,5 +1,5 @@
use crate::ErrorKind;
use crate::util::fetch::REQWEST_CLIENT;
use crate::util::fetch::INSECURE_REQWEST_CLIENT;
use base64::Engine;
use base64::prelude::{BASE64_STANDARD, BASE64_URL_SAFE_NO_PAD};
use chrono::{DateTime, Duration, TimeZone, Utc};
@@ -855,7 +855,7 @@ async fn oauth_token(
query.insert("scope", REQUESTED_SCOPE);
let res = auth_retry(|| {
REQWEST_CLIENT
INSECURE_REQWEST_CLIENT
.post("https://login.live.com/oauth20_token.srf")
.header("Accept", "application/json")
.form(&query)
@@ -903,7 +903,7 @@ async fn oauth_refresh(
query.insert("scope", REQUESTED_SCOPE);
let res = auth_retry(|| {
REQWEST_CLIENT
INSECURE_REQWEST_CLIENT
.post("https://login.live.com/oauth20_token.srf")
.header("Accept", "application/json")
.form(&query)
@@ -1048,7 +1048,7 @@ async fn minecraft_token(
let token = token.token;
let res = auth_retry(|| {
REQWEST_CLIENT
INSECURE_REQWEST_CLIENT
.post("https://api.minecraftservices.com/launcher/login")
.header("Accept", "application/json")
.json(&json!({
@@ -1276,7 +1276,7 @@ async fn minecraft_profile(
token: &str,
) -> Result<MinecraftProfile, MinecraftAuthenticationError> {
let res = auth_retry(|| {
REQWEST_CLIENT
INSECURE_REQWEST_CLIENT
.get("https://api.minecraftservices.com/minecraft/profile")
.header("Accept", "application/json")
.bearer_auth(token)
@@ -1327,7 +1327,7 @@ async fn minecraft_entitlements(
token: &str,
) -> Result<MinecraftEntitlements, MinecraftAuthenticationError> {
let res = auth_retry(|| {
REQWEST_CLIENT
INSECURE_REQWEST_CLIENT
.get(format!("https://api.minecraftservices.com/entitlements/license?requestId={}", Uuid::new_v4()))
.header("Accept", "application/json")
.bearer_auth(token)
@@ -1471,7 +1471,7 @@ async fn send_signed_request<T: DeserializeOwned>(
let signature = BASE64_STANDARD.encode(&sig_buffer);
let res = auth_retry(|| {
let mut request = REQWEST_CLIENT
let mut request = INSECURE_REQWEST_CLIENT
.post(url)
.header("Content-Type", "application/json; charset=utf-8")
.header("Accept", "application/json")

View File

@@ -11,7 +11,7 @@ use crate::{
ErrorKind,
data::Credentials,
state::{MinecraftProfile, PROFILE_CACHE, ProfileCacheEntry},
util::fetch::REQWEST_CLIENT,
util::fetch::INSECURE_REQWEST_CLIENT,
};
/// Provides operations for interacting with capes on a Minecraft player profile.
@@ -23,7 +23,7 @@ impl MinecraftCapeOperation {
cape_id: Uuid,
) -> crate::Result<()> {
update_profile_cache_from_response(
REQWEST_CLIENT
INSECURE_REQWEST_CLIENT
.put("https://api.minecraftservices.com/minecraft/profile/capes/active")
.header("Content-Type", "application/json; charset=utf-8")
.header("Accept", "application/json")
@@ -42,7 +42,7 @@ impl MinecraftCapeOperation {
pub async fn unequip_any(credentials: &Credentials) -> crate::Result<()> {
update_profile_cache_from_response(
REQWEST_CLIENT
INSECURE_REQWEST_CLIENT
.delete("https://api.minecraftservices.com/minecraft/profile/capes/active")
.header("Accept", "application/json")
.bearer_auth(&credentials.access_token)
@@ -92,7 +92,7 @@ impl MinecraftSkinOperation {
);
update_profile_cache_from_response(
REQWEST_CLIENT
INSECURE_REQWEST_CLIENT
.post(
"https://api.minecraftservices.com/minecraft/profile/skins",
)
@@ -110,7 +110,7 @@ impl MinecraftSkinOperation {
pub async fn unequip_any(credentials: &Credentials) -> crate::Result<()> {
update_profile_cache_from_response(
REQWEST_CLIENT
INSECURE_REQWEST_CLIENT
.delete("https://api.minecraftservices.com/minecraft/profile/skins/active")
.header("Accept", "application/json")
.bearer_auth(&credentials.access_token)

View File

@@ -130,18 +130,24 @@ static GLOBAL_FETCH_FENCE: LazyLock<FetchFence> =
inner: Mutex::new(FenceInner::new()),
});
pub static REQWEST_CLIENT: LazyLock<reqwest::Client> = LazyLock::new(|| {
let mut headers = reqwest::header::HeaderMap::new();
let header =
reqwest::header::HeaderValue::from_str(&crate::launcher_user_agent())
.unwrap();
headers.insert(reqwest::header::USER_AGENT, header);
fn reqwest_client_builder() -> reqwest::ClientBuilder {
reqwest::Client::builder()
.tcp_keepalive(Some(time::Duration::from_secs(10)))
.default_headers(headers)
.user_agent(crate::launcher_user_agent())
}
pub static INSECURE_REQWEST_CLIENT: LazyLock<reqwest::Client> =
LazyLock::new(|| {
reqwest_client_builder()
.build()
.expect("client configuration should be valid")
});
pub static REQWEST_CLIENT: LazyLock<reqwest::Client> = LazyLock::new(|| {
reqwest_client_builder()
.https_only(true)
.build()
.expect("Reqwest Client Building Failed")
.expect("client configuration should be valid")
});
const FETCH_ATTEMPTS: usize = 2;
@@ -157,6 +163,28 @@ pub async fn fetch(
.await
}
#[tracing::instrument(skip(semaphore))]
pub async fn fetch_with_client(
url: &str,
sha1: Option<&str>,
semaphore: &FetchSemaphore,
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>,
client: &reqwest::Client,
) -> crate::Result<Bytes> {
fetch_advanced_with_client(
Method::GET,
url,
sha1,
None,
None,
None,
semaphore,
exec,
client,
)
.await
}
#[tracing::instrument(skip(json_body, semaphore))]
pub async fn fetch_json<T>(
method: Method,
@@ -177,7 +205,8 @@ where
Ok(value)
}
/// Downloads a file with retry and checksum functionality
/// Downloads a file with retry and checksum functionality, and a specific
/// [`reqwest::Client`].
#[tracing::instrument(skip(json_body, semaphore))]
#[allow(clippy::too_many_arguments)]
pub async fn fetch_advanced(
@@ -189,6 +218,34 @@ pub async fn fetch_advanced(
loading_bar: Option<(&LoadingBarId, f64)>,
semaphore: &FetchSemaphore,
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>,
) -> crate::Result<Bytes> {
fetch_advanced_with_client(
method,
url,
sha1,
json_body,
header,
loading_bar,
semaphore,
exec,
&INSECURE_REQWEST_CLIENT,
)
.await
}
/// Downloads a file with retry and checksum functionality
#[tracing::instrument(skip(json_body, semaphore))]
#[allow(clippy::too_many_arguments)]
pub async fn fetch_advanced_with_client(
method: Method,
url: &str,
sha1: Option<&str>,
json_body: Option<serde_json::Value>,
header: Option<(&str, &str)>,
loading_bar: Option<(&LoadingBarId, f64)>,
semaphore: &FetchSemaphore,
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>,
client: &reqwest::Client,
) -> crate::Result<Bytes> {
let _permit = semaphore.0.acquire().await?;
@@ -210,7 +267,7 @@ pub async fn fetch_advanced(
return Err(ErrorKind::ApiIsDownError.into());
}
let mut req = REQWEST_CLIENT.request(method.clone(), url);
let mut req = INSECURE_REQWEST_CLIENT.request(method.clone(), url);
if let Some(body) = json_body.clone() {
req = req.json(&body);
@@ -328,7 +385,9 @@ pub async fn fetch_mirrors(
}
for (index, mirror) in mirrors.iter().enumerate() {
let result = fetch(mirror, sha1, semaphore, exec).await;
let result =
fetch_with_client(mirror, sha1, semaphore, exec, &REQWEST_CLIENT)
.await;
if result.is_ok() || (result.is_err() && index == (mirrors.len() - 1)) {
return result;
@@ -348,7 +407,7 @@ pub async fn post_json(
) -> crate::Result<()> {
let _permit = semaphore.0.acquire().await?;
let mut req = REQWEST_CLIENT.post(url).json(&json_body);
let mut req = INSECURE_REQWEST_CLIENT.post(url).json(&json_body);
if let Some(creds) =
crate::state::ModrinthCredentials::get_active(exec).await?