Add Minigames profile and Fabric admission in launcher 0.1.6
This commit is contained in:
Generated
+1
-1
@@ -3361,7 +3361,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "shacraft-launcher"
|
||||
version = "0.1.5"
|
||||
version = "0.1.6"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"ed25519-dalek",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "shacraft-launcher"
|
||||
version = "0.1.5"
|
||||
version = "0.1.6"
|
||||
description = "ShaCraft Minecraft launcher"
|
||||
authors = ["ShaCraft"]
|
||||
license = "MIT"
|
||||
|
||||
@@ -19,11 +19,21 @@ use zeroize::Zeroizing;
|
||||
|
||||
pub(crate) const TICKET_ENV: &str = "SHACRAFT_ADMISSION_TICKET";
|
||||
pub(crate) const PRIVATE_KEY_ENV: &str = "SHACRAFT_ADMISSION_PRIVATE_KEY";
|
||||
#[cfg(test)]
|
||||
const SERVER_ID: &str = "aoc";
|
||||
|
||||
pub(crate) fn server_for_profile(profile: &str) -> Result<&'static str, &'static str> {
|
||||
match profile {
|
||||
"aeronautics" => Ok("aoc"),
|
||||
"minigames" => Ok("minigames"),
|
||||
_ => Err("Unknown ShaCraft profile"),
|
||||
}
|
||||
}
|
||||
const MAX_LIFETIME_SECONDS: u64 = 600;
|
||||
|
||||
// Deliberately no Debug, Clone or Serialize for secret-bearing values.
|
||||
pub(crate) struct AdmissionKey {
|
||||
server_id: &'static str,
|
||||
public_key: String,
|
||||
private_key: Zeroizing<String>,
|
||||
}
|
||||
@@ -43,13 +53,15 @@ pub(crate) struct TicketResponse {
|
||||
}
|
||||
|
||||
pub(crate) struct Admission {
|
||||
server_id: &'static str,
|
||||
ticket_id: Zeroizing<String>,
|
||||
private_key: Zeroizing<String>,
|
||||
identity: PlayerIdentity,
|
||||
}
|
||||
|
||||
impl AdmissionKey {
|
||||
pub(crate) fn generate() -> Result<Self, &'static str> {
|
||||
pub(crate) fn generate(server_id: &'static str) -> Result<Self, &'static str> {
|
||||
if !matches!(server_id, "aoc" | "minigames") { return Err("Unknown ShaCraft server"); }
|
||||
let mut seed = Zeroizing::new([0_u8; 32]);
|
||||
getrandom::fill(seed.as_mut())
|
||||
.map_err(|_| "Не удалось создать защищённый ключ входа. Повторите запуск лаунчера.")?;
|
||||
@@ -65,6 +77,7 @@ impl AdmissionKey {
|
||||
.to_pkcs8_der()
|
||||
.map_err(|_| "Не удалось подготовить защищённый ключ входа.")?;
|
||||
Ok(Self {
|
||||
server_id,
|
||||
public_key,
|
||||
private_key: Zeroizing::new(STANDARD.encode(encoded.as_bytes())),
|
||||
})
|
||||
@@ -72,7 +85,7 @@ impl AdmissionKey {
|
||||
|
||||
pub(crate) fn request(&self) -> TicketRequest<'_> {
|
||||
TicketRequest {
|
||||
server_id: SERVER_ID,
|
||||
server_id: self.server_id,
|
||||
public_key: &self.public_key,
|
||||
}
|
||||
}
|
||||
@@ -92,12 +105,13 @@ impl AdmissionKey {
|
||||
.all(|byte| byte.is_ascii_alphanumeric() || byte == b'_');
|
||||
if !valid_ticket
|
||||
|| !valid_nickname
|
||||
|| response.server_id != SERVER_ID
|
||||
|| response.server_id != self.server_id
|
||||
|| !(1..=MAX_LIFETIME_SECONDS).contains(&response.expires_in_seconds)
|
||||
{
|
||||
return Err("Сервер вернул некорректное разрешение на вход. Повторите попытку позже.");
|
||||
}
|
||||
Ok(Admission {
|
||||
server_id: self.server_id,
|
||||
ticket_id,
|
||||
private_key: self.private_key,
|
||||
identity: PlayerIdentity::Offline {
|
||||
@@ -108,6 +122,7 @@ impl AdmissionKey {
|
||||
}
|
||||
|
||||
impl Admission {
|
||||
pub(crate) fn server_id(&self) -> &str { self.server_id }
|
||||
pub(crate) fn identity(&self) -> &PlayerIdentity {
|
||||
&self.identity
|
||||
}
|
||||
@@ -132,10 +147,25 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn profile_tickets_are_server_bound_with_shared_canonical_identity() {
|
||||
assert_eq!(server_for_profile("aeronautics").unwrap(), "aoc");
|
||||
assert_eq!(server_for_profile("minigames").unwrap(), "minigames");
|
||||
assert!(server_for_profile("../../other").is_err());
|
||||
assert!(AdmissionKey::generate("other").is_err());
|
||||
assert!(AdmissionKey::generate("minigames").unwrap().bind(response()).is_err());
|
||||
let key=AdmissionKey::generate("minigames").unwrap();
|
||||
assert_eq!(serde_json::to_value(key.request()).unwrap()["server_id"], "minigames");
|
||||
let mut payload=response(); payload.server_id="minigames".into();
|
||||
let admitted=key.bind(payload).unwrap();
|
||||
assert_eq!(admitted.server_id(), "minigames");
|
||||
assert_eq!(admitted.identity().name(), "Canonical_Name");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generates_distinct_keys_and_only_sends_the_public_key() {
|
||||
let key = AdmissionKey::generate().unwrap();
|
||||
let other = AdmissionKey::generate().unwrap();
|
||||
let key = AdmissionKey::generate("aoc").unwrap();
|
||||
let other = AdmissionKey::generate("aoc").unwrap();
|
||||
assert_ne!(key.public_key, other.public_key);
|
||||
let payload = serde_json::to_value(key.request()).unwrap();
|
||||
assert_eq!(payload.as_object().unwrap().len(), 2);
|
||||
@@ -173,7 +203,7 @@ mod tests {
|
||||
for mutate in mutations {
|
||||
let mut payload = response();
|
||||
mutate(&mut payload);
|
||||
assert!(AdmissionKey::generate().unwrap().bind(payload).is_err());
|
||||
assert!(AdmissionKey::generate("aoc").unwrap().bind(payload).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,7 +211,7 @@ mod tests {
|
||||
fn secrets_only_enter_the_child_environment_and_identity_comes_from_ticket() {
|
||||
let original_ticket = std::env::var_os(TICKET_ENV);
|
||||
let original_key = std::env::var_os(PRIVATE_KEY_ENV);
|
||||
let admission = AdmissionKey::generate().unwrap().bind(response()).unwrap();
|
||||
let admission = AdmissionKey::generate("aoc").unwrap().bind(response()).unwrap();
|
||||
let mut command = Command::new("java");
|
||||
command
|
||||
.arg("-Xmx6144M")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::data_dir;
|
||||
use crate::{
|
||||
java, launch, manifest, mojang, neoforge, operations::LauncherOperations, remote, runtime,
|
||||
fabric, java, launch, manifest, mojang, neoforge, operations::LauncherOperations, remote, runtime,
|
||||
settings, shacraft_account,
|
||||
};
|
||||
use reqwest::blocking::Client;
|
||||
@@ -56,6 +56,9 @@ fn resolve_merged_version(
|
||||
)
|
||||
.map_err(|error| error.to_string())?;
|
||||
mojang::merge_versions(&vanilla, Some(&neoforge_version)).map_err(|error| error.to_string())
|
||||
} else if manifest.minecraft.loader.kind == "fabric" {
|
||||
let child = fabric::fetch_profile(&manifest.minecraft.version, &manifest.minecraft.loader.version)?;
|
||||
mojang::merge_versions(&vanilla, Some(&child)).map_err(|error| error.to_string())
|
||||
} else {
|
||||
mojang::merge_versions(&vanilla, None).map_err(|error| error.to_string())
|
||||
}
|
||||
@@ -205,7 +208,7 @@ pub(crate) async fn launch_game(
|
||||
// through spawn so local logout/account switching cannot race issuance.
|
||||
let _account_permit = account_operation.acquire("ShaCraft account operation")?;
|
||||
let admission =
|
||||
shacraft_account::issue_admission(&data_dir).map_err(|error| error.to_string())?;
|
||||
shacraft_account::issue_admission(&data_dir, crate::admission::server_for_profile(&profile_id)?).map_err(|error| error.to_string())?;
|
||||
let request = launch::LaunchRequest {
|
||||
java_executable: Path::new(&java_install.executable),
|
||||
game_dir: &game_dir,
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
//! Fabric metadata and Maven are fixed, independent game trust domains.
|
||||
//! The signed ShaCraft manifest selects versions, never arbitrary loader URLs.
|
||||
use crate::mojang::{Artifact, LibraryDownloads, VersionJson};
|
||||
use reqwest::blocking::Client;
|
||||
use serde::Deserialize;
|
||||
use std::{io::Read, time::Duration};
|
||||
|
||||
pub(crate) const MAVEN_HOST: &str = "maven.fabricmc.net";
|
||||
const HOSTS: [&str; 2] = ["meta.fabricmc.net", MAVEN_HOST];
|
||||
const MAX_PROFILE: usize = 512 * 1024;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct FabricLibrary {
|
||||
name: String,
|
||||
url: String,
|
||||
sha1: Option<String>,
|
||||
size: Option<u64>,
|
||||
}
|
||||
|
||||
fn coordinate_path(name: &str) -> Result<String, String> {
|
||||
let pieces: Vec<_> = name.split(':').collect();
|
||||
if pieces.len() != 3
|
||||
|| pieces.iter().any(|p| {
|
||||
!crate::manifest::is_portable_component(p)
|
||||
|| *p == "."
|
||||
|| *p == ".."
|
||||
|| !p
|
||||
.bytes()
|
||||
.all(|c| c.is_ascii_alphanumeric() || b"._+-".contains(&c))
|
||||
})
|
||||
|| pieces[0].split('.').any(|p| !crate::manifest::is_portable_component(p))
|
||||
{
|
||||
return Err("Invalid Fabric library coordinate".into());
|
||||
}
|
||||
Ok(format!(
|
||||
"{}/{}/{}/{}-{}.jar",
|
||||
pieces[0].replace('.', "/"),
|
||||
pieces[1],
|
||||
pieces[2],
|
||||
pieces[1],
|
||||
pieces[2]
|
||||
))
|
||||
}
|
||||
|
||||
fn get(client: &Client, url: &str, limit: usize) -> Result<Vec<u8>, String> {
|
||||
let response = client
|
||||
.get(url)
|
||||
.send()
|
||||
.map_err(|e| e.to_string())?
|
||||
.error_for_status()
|
||||
.map_err(|e| e.to_string())?;
|
||||
let mut bytes = Vec::new();
|
||||
response
|
||||
.take((limit + 1) as u64)
|
||||
.read_to_end(&mut bytes)
|
||||
.map_err(|e| e.to_string())?;
|
||||
if bytes.len() > limit {
|
||||
return Err("Fabric metadata exceeds its size limit".into());
|
||||
}
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
pub(crate) fn fetch_profile(minecraft: &str, loader: &str) -> Result<VersionJson, String> {
|
||||
let client =
|
||||
crate::trusted_http::client(&HOSTS, Duration::from_secs(30)).map_err(|e| e.to_string())?;
|
||||
// Defence in depth: these values normally already passed manifest validation.
|
||||
if [minecraft, loader].iter().any(|v| {
|
||||
!crate::manifest::is_portable_component(v)
|
||||
|| v.contains('/')
|
||||
|| !v
|
||||
.bytes()
|
||||
.all(|c| c.is_ascii_alphanumeric() || b"._+-".contains(&c))
|
||||
}) {
|
||||
return Err("Invalid Fabric version".into());
|
||||
}
|
||||
let bytes = get(
|
||||
&client,
|
||||
&format!("https://meta.fabricmc.net/v2/versions/loader/{minecraft}/{loader}/profile/json"),
|
||||
MAX_PROFILE,
|
||||
)?;
|
||||
normalize(&client, &bytes, minecraft, loader)
|
||||
}
|
||||
|
||||
fn normalize(
|
||||
client: &Client,
|
||||
bytes: &[u8],
|
||||
minecraft: &str,
|
||||
loader: &str,
|
||||
) -> Result<VersionJson, String> {
|
||||
// Flattening libraries would consume the same key twice; parse the two views explicitly.
|
||||
let value: serde_json::Value = serde_json::from_slice(bytes).map_err(|e| e.to_string())?;
|
||||
let parent = value.get("inheritsFrom").and_then(|v| v.as_str());
|
||||
let mut version: VersionJson =
|
||||
serde_json::from_value(value.clone()).map_err(|e| e.to_string())?;
|
||||
if parent != Some(minecraft)
|
||||
|| version.id != format!("fabric-loader-{loader}-{minecraft}")
|
||||
|| version.main_class != "net.fabricmc.loader.impl.launch.knot.KnotClient"
|
||||
{
|
||||
return Err("Fabric profile identity mismatch".into());
|
||||
}
|
||||
let artifacts: Vec<FabricLibrary> =
|
||||
serde_json::from_value(value["libraries"].clone()).map_err(|e| e.to_string())?;
|
||||
if artifacts.is_empty() || artifacts.len() > 32 {
|
||||
return Err("Invalid Fabric library count".into());
|
||||
}
|
||||
for (library, artifact) in version.libraries.iter_mut().zip(artifacts) {
|
||||
if artifact.url != "https://maven.fabricmc.net/" {
|
||||
return Err("Untrusted Fabric Maven URL".into());
|
||||
}
|
||||
let path = coordinate_path(&artifact.name)?;
|
||||
let url = format!("https://{MAVEN_HOST}/{path}");
|
||||
let sha1 = match artifact.sha1 {
|
||||
Some(hash) => hash,
|
||||
None => String::from_utf8(get(client, &format!("{url}.sha1"), 128)?)
|
||||
.map_err(|e| e.to_string())?
|
||||
.trim()
|
||||
.to_owned(),
|
||||
};
|
||||
let size = match artifact.size {
|
||||
Some(size) => size,
|
||||
None => client
|
||||
.head(&url)
|
||||
.send()
|
||||
.map_err(|e| e.to_string())?
|
||||
.error_for_status()
|
||||
.map_err(|e| e.to_string())?
|
||||
.headers()
|
||||
.get(reqwest::header::CONTENT_LENGTH)
|
||||
.and_then(|h| h.to_str().ok())
|
||||
.and_then(|s| s.parse::<u64>().ok())
|
||||
.ok_or("Fabric library has no size")?,
|
||||
};
|
||||
if sha1.len() != 40
|
||||
|| !sha1.bytes().all(|b| b.is_ascii_hexdigit())
|
||||
|| size == 0
|
||||
|| size > 64 * 1024 * 1024
|
||||
{
|
||||
return Err("Invalid Fabric library hash or size".into());
|
||||
}
|
||||
library.downloads = Some(LibraryDownloads {
|
||||
artifact: Some(Artifact {
|
||||
path,
|
||||
url,
|
||||
sha1,
|
||||
size,
|
||||
}),
|
||||
});
|
||||
}
|
||||
Ok(version)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn maven_coordinates_cannot_escape_library_directory() {
|
||||
assert_eq!(
|
||||
coordinate_path("net.fabricmc:fabric-loader:0.19.5").unwrap(),
|
||||
"net/fabricmc/fabric-loader/0.19.5/fabric-loader-0.19.5.jar"
|
||||
);
|
||||
for bad in [
|
||||
"x:y:../evil",
|
||||
"a..b:c:1",
|
||||
"x:/tmp:1",
|
||||
"x:y:1:extra",
|
||||
"x:y:\\evil",
|
||||
"x:y:..",
|
||||
"CON:y:1",
|
||||
"a:y.:1",
|
||||
"a:AUX:1",
|
||||
] {
|
||||
assert!(coordinate_path(bad).is_err(), "{bad}");
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn loader_identity_and_maven_origin_are_checked_before_downloads() {
|
||||
let client = Client::new();
|
||||
let base = serde_json::json!({"id":"fabric-loader-0.19.5-26.2", "inheritsFrom":"26.2", "mainClass":"net.fabricmc.loader.impl.launch.knot.KnotClient", "libraries":[{"name":"net.fabricmc:fabric-loader:0.19.5", "url":"https://maven.fabricmc.net/", "sha1":"a".repeat(40), "size":42}]});
|
||||
assert!(normalize(
|
||||
&client,
|
||||
&serde_json::to_vec(&base).unwrap(),
|
||||
"26.2",
|
||||
"0.19.5"
|
||||
)
|
||||
.is_ok());
|
||||
for (field, value) in [
|
||||
("inheritsFrom", "1.21.1"),
|
||||
("mainClass", "attacker.Main"),
|
||||
("id", "wrong"),
|
||||
] {
|
||||
let mut bad = base.clone();
|
||||
bad[field] = value.into();
|
||||
assert!(normalize(
|
||||
&client,
|
||||
&serde_json::to_vec(&bad).unwrap(),
|
||||
"26.2",
|
||||
"0.19.5"
|
||||
)
|
||||
.is_err());
|
||||
}
|
||||
let mut bad = base;
|
||||
bad["libraries"][0]["url"] = "https://attacker.invalid/".into();
|
||||
assert!(normalize(
|
||||
&client,
|
||||
&serde_json::to_vec(&bad).unwrap(),
|
||||
"26.2",
|
||||
"0.19.5"
|
||||
)
|
||||
.is_err());
|
||||
}
|
||||
#[test]
|
||||
#[ignore = "downloads official Fabric profile metadata"]
|
||||
fn live_resolves_fabric_26_2() {
|
||||
let profile = fetch_profile("26.2", "0.19.5").unwrap();
|
||||
assert!(profile
|
||||
.libraries
|
||||
.iter()
|
||||
.all(|library| library.downloads.is_some()));
|
||||
}
|
||||
}
|
||||
+57
-50
@@ -245,6 +245,10 @@ fn build_command(request: &LaunchRequest) -> Result<Command, LaunchError> {
|
||||
for argument in game_args {
|
||||
command.arg(substitute(&argument, &vars));
|
||||
}
|
||||
// This endpoint is native-owned; a manifest cannot redirect game admission.
|
||||
if request.admission.server_id() == "minigames" {
|
||||
command.args(["--quickPlayMultiplayer", "135.106.154.86:25568"]);
|
||||
}
|
||||
command.current_dir(request.profile_dir);
|
||||
|
||||
let log_file = fs::File::create(request.log_path)?;
|
||||
@@ -268,59 +272,62 @@ mod tests {
|
||||
let game_dir = directory.join("game");
|
||||
let profile_dir = directory.join("profile");
|
||||
let log_path = directory.join("game.log");
|
||||
let vanilla: mojang::VersionJson = serde_json::from_value(serde_json::json!({
|
||||
"id": "1.21.1",
|
||||
"mainClass": "net.minecraft.client.main.Main",
|
||||
"arguments": {
|
||||
"game": ["--username", "${auth_player_name}", "--uuid", "${auth_uuid}", "--accessToken", "${auth_access_token}"],
|
||||
"jvm": ["-cp", "${classpath}", "-Dlauncher=${launcher_name}"]
|
||||
},
|
||||
"assetIndex": {"id": "17", "sha1": "0".repeat(40), "size": 1, "url": "https://piston-meta.mojang.com/assets"},
|
||||
"downloads": {"client": {"sha1": "0".repeat(40), "size": 1, "url": "https://piston-data.mojang.com/client.jar"}}
|
||||
})).unwrap();
|
||||
let merged = mojang::merge_versions(&vanilla, None).unwrap();
|
||||
let response = serde_json::from_value(serde_json::json!({
|
||||
"ticket_id": URL_SAFE_NO_PAD.encode([73_u8; 32]), "mc_username": "Ticket_Name",
|
||||
"server_id": "aoc", "expires_in_seconds": 600
|
||||
}))
|
||||
.unwrap();
|
||||
let admission = AdmissionKey::generate().unwrap().bind(response).unwrap();
|
||||
let request = LaunchRequest {
|
||||
java_executable: Path::new("java"),
|
||||
game_dir: &game_dir,
|
||||
profile_dir: &profile_dir,
|
||||
merged: &merged,
|
||||
admission: &admission,
|
||||
memory_mb: 6144,
|
||||
log_path: &log_path,
|
||||
};
|
||||
let command = build_command(&request).unwrap();
|
||||
let env: HashMap<_, _> = command.get_envs().collect();
|
||||
let proof = [TICKET_ENV, PRIVATE_KEY_ENV]
|
||||
.map(|name| env[std::ffi::OsStr::new(name)].unwrap().to_str().unwrap());
|
||||
let arguments: Vec<_> = command
|
||||
.get_args()
|
||||
.map(|arg| arg.to_string_lossy())
|
||||
.collect();
|
||||
assert!(arguments
|
||||
.windows(2)
|
||||
.any(|args| args == ["--username", "Ticket_Name"]));
|
||||
assert!(arguments
|
||||
.windows(2)
|
||||
.any(|args| args == ["--accessToken", "0"]));
|
||||
for secret in proof {
|
||||
assert!(arguments.iter().all(|argument| !argument.contains(secret)));
|
||||
for path in [
|
||||
log_path.clone(),
|
||||
game_dir.join(".shacraft-client-id"),
|
||||
profile_dir.join(".shacraft-jvm.args"),
|
||||
] {
|
||||
if path.exists() {
|
||||
assert!(!fs::read_to_string(path).unwrap().contains(secret));
|
||||
for server_id in ["aoc", "minigames"] {
|
||||
let vanilla: mojang::VersionJson = serde_json::from_value(serde_json::json!({
|
||||
"id": "1.21.1",
|
||||
"mainClass": "net.minecraft.client.main.Main",
|
||||
"arguments": {
|
||||
"game": ["--username", "${auth_player_name}", "--uuid", "${auth_uuid}", "--accessToken", "${auth_access_token}"],
|
||||
"jvm": ["-cp", "${classpath}", "-Dlauncher=${launcher_name}"]
|
||||
},
|
||||
"assetIndex": {"id": "17", "sha1": "0".repeat(40), "size": 1, "url": "https://piston-meta.mojang.com/assets"},
|
||||
"downloads": {"client": {"sha1": "0".repeat(40), "size": 1, "url": "https://piston-data.mojang.com/client.jar"}}
|
||||
})).unwrap();
|
||||
let merged = mojang::merge_versions(&vanilla, None).unwrap();
|
||||
let response = serde_json::from_value(serde_json::json!({
|
||||
"ticket_id": URL_SAFE_NO_PAD.encode([73_u8; 32]), "mc_username": "Ticket_Name",
|
||||
"server_id": server_id, "expires_in_seconds": 600
|
||||
}))
|
||||
.unwrap();
|
||||
let admission = AdmissionKey::generate(server_id).unwrap().bind(response).unwrap();
|
||||
let request = LaunchRequest {
|
||||
java_executable: Path::new("java"),
|
||||
game_dir: &game_dir,
|
||||
profile_dir: &profile_dir,
|
||||
merged: &merged,
|
||||
admission: &admission,
|
||||
memory_mb: 6144,
|
||||
log_path: &log_path,
|
||||
};
|
||||
let command = build_command(&request).unwrap();
|
||||
let env: HashMap<_, _> = command.get_envs().collect();
|
||||
let proof = [TICKET_ENV, PRIVATE_KEY_ENV]
|
||||
.map(|name| env[std::ffi::OsStr::new(name)].unwrap().to_str().unwrap());
|
||||
let arguments: Vec<_> = command
|
||||
.get_args()
|
||||
.map(|arg| arg.to_string_lossy())
|
||||
.collect();
|
||||
assert!(arguments
|
||||
.windows(2)
|
||||
.any(|args| args == ["--username", "Ticket_Name"]));
|
||||
assert!(arguments
|
||||
.windows(2)
|
||||
.any(|args| args == ["--accessToken", "0"]));
|
||||
assert_eq!(arguments.windows(2).any(|pair| pair == ["--quickPlayMultiplayer", "135.106.154.86:25568"]), server_id == "minigames");
|
||||
for secret in proof {
|
||||
assert!(arguments.iter().all(|argument| !argument.contains(secret)));
|
||||
for path in [
|
||||
log_path.clone(),
|
||||
game_dir.join(".shacraft-client-id"),
|
||||
profile_dir.join(".shacraft-jvm.args"),
|
||||
] {
|
||||
if path.exists() {
|
||||
assert!(!fs::read_to_string(path).unwrap().contains(secret));
|
||||
}
|
||||
}
|
||||
}
|
||||
drop(command);
|
||||
}
|
||||
drop(command);
|
||||
fs::remove_dir_all(directory).unwrap();
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ mod admission;
|
||||
#[cfg(target_os = "linux")]
|
||||
mod deb_updater;
|
||||
mod download;
|
||||
mod fabric;
|
||||
mod java;
|
||||
mod launch;
|
||||
mod manifest;
|
||||
|
||||
@@ -91,7 +91,7 @@ fn validate(manifest: &Manifest) -> Result<(), ManifestError> {
|
||||
));
|
||||
}
|
||||
if !is_version(&manifest.minecraft.version)
|
||||
|| manifest.minecraft.loader.kind.trim().is_empty()
|
||||
|| !matches!(manifest.minecraft.loader.kind.as_str(), "neoforge" | "fabric" | "vanilla")
|
||||
|| !is_version(&manifest.minecraft.loader.version)
|
||||
{
|
||||
return Err(ManifestError::Invalid(
|
||||
|
||||
@@ -58,6 +58,7 @@ pub fn library_http_client() -> Result<Client, reqwest::Error> {
|
||||
MOJANG_HOSTS[2],
|
||||
MOJANG_HOSTS[3],
|
||||
crate::neoforge::NEOFORGE_HOST,
|
||||
crate::fabric::MAVEN_HOST,
|
||||
],
|
||||
std::time::Duration::from_secs(10 * 60),
|
||||
)
|
||||
@@ -67,7 +68,7 @@ pub fn library_http_client() -> Result<Client, reqwest::Error> {
|
||||
/// own fixed Maven. The profile itself comes from the SHA-256-verified
|
||||
/// NeoForge installer, never from the ShaCraft manifest.
|
||||
fn is_allowed_library_host(url: &str) -> bool {
|
||||
is_allowed_host(url) || crate::neoforge::is_allowed_host(url)
|
||||
is_allowed_host(url) || crate::neoforge::is_allowed_host(url) || crate::trusted_http::allows(url, &[crate::fabric::MAVEN_HOST])
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
||||
@@ -13,6 +13,8 @@ use std::{
|
||||
|
||||
const AERONAUTICS_MANIFEST: &str =
|
||||
"https://shacraft.ru/api/launcher/v2/profiles/aeronautics/signed-manifest";
|
||||
const MINIGAMES_MANIFEST: &str = "https://shacraft.ru/api/launcher/v2/profiles/minigames/signed-manifest";
|
||||
const MINIGAMES_ONLINE: &str = "https://shacraft.ru/api/online/minigames";
|
||||
const AERONAUTICS_ONLINE: &str = "https://shacraft.ru/api/online/aoc";
|
||||
const MANIFEST_PUBLIC_KEY: &str = "2S3FRdZj4Xw5nJpZ3IhqVITBg3nTH9AtGSo1Ew9+qVQ=";
|
||||
const MANIFEST_KEY_ID: &str = "2026-09-06";
|
||||
@@ -71,6 +73,7 @@ impl fmt::Display for RemoteError {
|
||||
pub fn fetch_manifest(profile_id: &str) -> Result<Manifest, RemoteError> {
|
||||
let url = match profile_id {
|
||||
"aeronautics" => AERONAUTICS_MANIFEST,
|
||||
"minigames" => MINIGAMES_MANIFEST,
|
||||
_ => return Err(RemoteError::UnknownProfile),
|
||||
};
|
||||
let client = Client::builder()
|
||||
@@ -125,6 +128,7 @@ fn fetch_manifest_bytes(client: &Client, url: &str) -> Result<Vec<u8>, RemoteErr
|
||||
pub fn fetch_server_status(profile_id: &str) -> Result<ServerStatus, RemoteError> {
|
||||
let url = match profile_id {
|
||||
"aeronautics" => AERONAUTICS_ONLINE,
|
||||
"minigames" => MINIGAMES_ONLINE,
|
||||
_ => return Err(RemoteError::UnknownProfile),
|
||||
};
|
||||
let client = Client::builder()
|
||||
|
||||
@@ -274,9 +274,10 @@ pub fn claim_nickname(data_dir: &Path, nickname: &str) -> Result<Account, Accoun
|
||||
/// this response is exposed to the webview or persisted with account settings.
|
||||
pub(crate) fn issue_admission(
|
||||
data_dir: &Path,
|
||||
server_id: &'static str,
|
||||
) -> Result<crate::admission::Admission, AccountError> {
|
||||
let token = load_session(data_dir)?;
|
||||
let key = crate::admission::AdmissionKey::generate()
|
||||
let key = crate::admission::AdmissionKey::generate(server_id)
|
||||
.map_err(|message| AccountError::Api(message.into()))?;
|
||||
let response = client()?
|
||||
.post(format!("{API_ORIGIN}{ADMISSION_ENDPOINT}"))
|
||||
@@ -359,7 +360,7 @@ mod tests {
|
||||
let directory = temporary_directory();
|
||||
crate::settings::save(&directory, crate::settings::LauncherSettings::default()).unwrap();
|
||||
assert!(matches!(
|
||||
issue_admission(&directory),
|
||||
issue_admission(&directory, "aoc"),
|
||||
Err(AccountError::InvalidSession)
|
||||
));
|
||||
fs::remove_dir_all(directory).unwrap();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "ShaCraft Launcher",
|
||||
"version": "0.1.5",
|
||||
"version": "0.1.6",
|
||||
"identifier": "ru.shacraft.launcher",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
|
||||
Reference in New Issue
Block a user