fix(skins): fix skin lookup across directories, reliable TCP flush, modern userProperties, and optimize compile time

This commit is contained in:
katR 2026-10-11 09:13:55 +05:30
parent e06775aae5
commit a8826b83d3
6 changed files with 154 additions and 133 deletions

View file

@ -1,3 +1,7 @@
[target.x86_64-unknown-linux-gnu]
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=lld"]
# Linux and Macos by default have 8MB of stack storage. # Linux and Macos by default have 8MB of stack storage.
# Windows has 1MB, it isn't enough, so it's changed to 8MB also. # Windows has 1MB, it isn't enough, so it's changed to 8MB also.
[target.x86_64-pc-windows-msvc] [target.x86_64-pc-windows-msvc]

View file

@ -151,8 +151,8 @@ freya-skia-safe = "=0.103.2" # required for the SKSL player renderer
image = {version = "=0.25.10", default-features = false, features = ["png", "jpeg"]} image = {version = "=0.25.10", default-features = false, features = ["png", "jpeg"]}
[profile.release] [profile.release]
codegen-units = 1 codegen-units = 16
debug = "line-tables-only" debug = false
lto = "thin" lto = "thin"
opt-level = 3 opt-level = 3
panic = "abort" panic = "abort"
@ -174,5 +174,9 @@ lto = false
strip = false strip = false
[profile.dev] [profile.dev]
opt-level = 0
debug = 0 debug = 0
strip = "debuginfo" strip = "debuginfo"
[profile.dev.package."*"]
opt-level = 2

View file

@ -150,9 +150,6 @@ pub async fn save_account_skin(
.await .await
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
let skin_path = skin_file_path(uuid).map_err(|e| e.to_string())?;
let meta_path = skin_meta_path(uuid).map_err(|e| e.to_string())?;
let img = image::load_from_memory(skin_bytes) let img = image::load_from_memory(skin_bytes)
.map_err(|e| format!("Invalid image format: {e}"))? .map_err(|e| format!("Invalid image format: {e}"))?
.to_rgba8(); .to_rgba8();
@ -177,17 +174,20 @@ pub async fn save_account_skin(
.write_to(&mut cursor, image::ImageFormat::Png) .write_to(&mut cursor, image::ImageFormat::Png)
.map_err(|e| format!("Failed to encode PNG: {e}"))?; .map_err(|e| format!("Failed to encode PNG: {e}"))?;
polyio::write(&skin_path, &png_bytes)
.await
.map_err(|e| format!("Failed to write skin file: {e}"))?;
let meta = SkinMetadata { is_slim }; let meta = SkinMetadata { is_slim };
let meta_json = serde_json::to_string_pretty(&meta) let meta_json = serde_json::to_string_pretty(&meta)
.map_err(|e| format!("Failed to serialize metadata: {e}"))?; .map_err(|e| format!("Failed to serialize metadata: {e}"))?;
polyio::write(&meta_path, meta_json.as_bytes()) for dir in oneclient_common::paths::all_skins_dirs() {
.await let _ = polyio::create_dir_all(&dir).await;
.map_err(|e| format!("Failed to write skin metadata: {e}"))?; let _ = polyio::write(&dir.join(format!("{uuid}.png")), &png_bytes).await;
let _ = polyio::write(&dir.join(format!("{uuid}.json")), meta_json.as_bytes()).await;
let _ = polyio::write(&dir.join(format!("{uuid}.meta.json")), meta_json.as_bytes()).await;
if let Some(uname) = username.filter(|u| !u.is_empty()) {
let _ = polyio::write(&dir.join(format!("{uname}.png")), &png_bytes).await;
let _ = polyio::write(&dir.join(format!("{uname}.json")), meta_json.as_bytes()).await;
}
}
if let Some(uname) = username.filter(|u| !u.is_empty()) { if let Some(uname) = username.filter(|u| !u.is_empty()) {
let _ = sync_custom_skin_loader(uname, &png_bytes).await; let _ = sync_custom_skin_loader(uname, &png_bytes).await;
@ -199,14 +199,14 @@ pub async fn save_account_skin(
} }
pub async fn delete_account_skin(uuid: &str, username: Option<&str>) -> Result<(), String> { pub async fn delete_account_skin(uuid: &str, username: Option<&str>) -> Result<(), String> {
let skin_path = skin_file_path(uuid).map_err(|e| e.to_string())?; for dir in oneclient_common::paths::all_skins_dirs() {
let meta_path = skin_meta_path(uuid).map_err(|e| e.to_string())?; let _ = polyio::remove_file(&dir.join(format!("{uuid}.png"))).await;
let _ = polyio::remove_file(&dir.join(format!("{uuid}.json"))).await;
if skin_path.exists() { let _ = polyio::remove_file(&dir.join(format!("{uuid}.meta.json"))).await;
let _ = polyio::remove_file(&skin_path).await; if let Some(uname) = username.filter(|u| !u.is_empty()) {
} let _ = polyio::remove_file(&dir.join(format!("{uname}.png"))).await;
if meta_path.exists() { let _ = polyio::remove_file(&dir.join(format!("{uname}.json"))).await;
let _ = polyio::remove_file(&meta_path).await; }
} }
if let Some(uname) = username.filter(|u| !u.is_empty()) { if let Some(uname) = username.filter(|u| !u.is_empty()) {

View file

@ -116,6 +116,27 @@ pub fn skins_dir() -> PathsResult<PathBuf> {
Ok(data_dir()?.join("skins")) Ok(data_dir()?.join("skins"))
} }
/// Returns all candidate skins directories across current data dir, release, dev, and legacy paths
pub fn all_skins_dirs() -> Vec<PathBuf> {
let mut dirs = Vec::new();
if let Ok(dir) = skins_dir() {
dirs.push(dir);
}
if let Some(base) = BaseDirs::new() {
let data = base.data_dir();
for candidate in [
data.join("Polyfrost").join("OneClient").join("skins"),
data.join("oneclient-dev").join("skins"),
data.join("oneclient").join("skins"),
] {
if !dirs.contains(&candidate) {
dirs.push(candidate);
}
}
}
dirs
}
pub fn skin_file_path(uuid: &str) -> PathsResult<PathBuf> { pub fn skin_file_path(uuid: &str) -> PathsResult<PathBuf> {
Ok(skins_dir()?.join(format!("{uuid}.png"))) Ok(skins_dir()?.join(format!("{uuid}.png")))
} }

View file

@ -317,7 +317,14 @@ async fn start(
let mods_dir = paths::cluster_mods_dir(&cluster.folder_name)?; let mods_dir = paths::cluster_mods_dir(&cluster.folder_name)?;
if account.is_offline() { if account.is_offline() {
crate::game::skin_server::disable_incompatible_offline_mods(&[mods_dir.clone(), cwd.join("mods")]).await; let mut scan_dirs = vec![mods_dir.clone(), cwd.join("mods")];
if let Ok(shared_mods) = paths::shared_mods_dir() {
scan_dirs.push(shared_mods);
}
if let Ok(shared_mc) = paths::shared_minecraft_dir() {
scan_dirs.push(shared_mc.join("mods"));
}
crate::game::skin_server::disable_incompatible_offline_mods(&scan_dirs).await;
if cluster.mc_loader == oneclient_common::domain::GameLoader::Fabric if cluster.mc_loader == oneclient_common::domain::GameLoader::Fabric
&& version.id == "1.21.11" && version.id == "1.21.11"
{ {
@ -423,12 +430,18 @@ async fn start(
&& let Some(user_props) = && let Some(user_props) =
crate::game::skin_server::build_user_properties(&account.id.to_string(), port).await crate::game::skin_server::build_user_properties(&account.id.to_string(), port).await
{ {
let mut replaced = false;
for i in 0..mc_args.len() { for i in 0..mc_args.len() {
if mc_args[i] == "--userProperties" && i + 1 < mc_args.len() && mc_args[i + 1] == "{}" { if mc_args[i] == "--userProperties" && i + 1 < mc_args.len() {
mc_args[i + 1] = user_props; mc_args[i + 1] = user_props.clone();
replaced = true;
break; break;
} }
} }
if !replaced {
mc_args.push("--userProperties".to_string());
mc_args.push(user_props);
}
} }
arguments::append_profile_game_arguments( arguments::append_profile_game_arguments(
&mut mc_args, &mut mc_args,

View file

@ -5,7 +5,7 @@ use std::time::{Duration, Instant};
use base64::Engine; use base64::Engine;
use oneclient_common::paths::{ use oneclient_common::paths::{
authlib_injector_path, shared_minecraft_dir, skin_file_path, skin_meta_path, skins_dir, authlib_injector_path, shared_minecraft_dir,
}; };
use parking_lot::RwLock; use parking_lot::RwLock;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -185,9 +185,7 @@ async fn handle_connection(mut stream: TcpStream, port: u16) -> Result<(), std::
tracing::info!(method, uri, "skin_server incoming request"); tracing::info!(method, uri, "skin_server incoming request");
if method != "GET" && method != "HEAD" && method != "POST" { if method != "GET" && method != "HEAD" && method != "POST" {
let response = "HTTP/1.1 204 No Content\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"; return send_no_content(stream).await;
stream.write_all(response.as_bytes()).await?;
return Ok(());
} }
let path = uri.split('?').next().unwrap_or(uri); let path = uri.split('?').next().unwrap_or(uri);
@ -200,16 +198,10 @@ async fn handle_connection(mut stream: TcpStream, port: u16) -> Result<(), std::
"implementationName": "onelauncher-skin-server", "implementationName": "onelauncher-skin-server",
"implementationVersion": env!("CARGO_PKG_VERSION") "implementationVersion": env!("CARGO_PKG_VERSION")
}, },
"skinDomains": ["127.0.0.1", "localhost"], "skinDomains": ["127.0.0.1", "localhost", ".minecraft.net", ".mojang.com"],
"signaturePublickey": SKIN_SERVER_PUBLIC_KEY "signaturePublickey": SKIN_SERVER_PUBLIC_KEY
}).to_string(); }).to_string();
let header = format!( return send_response(stream, "200 OK", "application/json; charset=utf-8", body.as_bytes()).await;
"HTTP/1.1 200 OK\r\nContent-Type: application/json; charset=utf-8\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
body.len(),
body
);
stream.write_all(header.as_bytes()).await?;
return Ok(());
} }
// 2. Public keys endpoint (for Minecraft 1.19+ profile signature verification) // 2. Public keys endpoint (for Minecraft 1.19+ profile signature verification)
@ -219,13 +211,7 @@ async fn handle_connection(mut stream: TcpStream, port: u16) -> Result<(), std::
"profilePropertyKeys": [{"publicKey": key_b64}], "profilePropertyKeys": [{"publicKey": key_b64}],
"playerCertificateKeys": [{"publicKey": key_b64}] "playerCertificateKeys": [{"publicKey": key_b64}]
}).to_string(); }).to_string();
let header = format!( return send_response(stream, "200 OK", "application/json; charset=utf-8", body.as_bytes()).await;
"HTTP/1.1 200 OK\r\nContent-Type: application/json; charset=utf-8\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
body.len(),
body
);
stream.write_all(header.as_bytes()).await?;
return Ok(());
} }
// 3. Profiles by names endpoint (bulk profile lookup) // 3. Profiles by names endpoint (bulk profile lookup)
@ -243,13 +229,7 @@ async fn handle_connection(mut stream: TcpStream, port: u16) -> Result<(), std::
} }
} }
let body = serde_json::to_string(&profiles).unwrap_or_else(|_| "[]".to_string()); let body = serde_json::to_string(&profiles).unwrap_or_else(|_| "[]".to_string());
let header = format!( return send_response(stream, "200 OK", "application/json; charset=utf-8", body.as_bytes()).await;
"HTTP/1.1 200 OK\r\nContent-Type: application/json; charset=utf-8\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
body.len(),
body
);
stream.write_all(header.as_bytes()).await?;
return Ok(());
} }
// 4. Profile request: /sessionserver/session/minecraft/profile/<uuid> or /session/minecraft/profile/<uuid> // 4. Profile request: /sessionserver/session/minecraft/profile/<uuid> or /session/minecraft/profile/<uuid>
@ -262,50 +242,58 @@ async fn handle_connection(mut stream: TcpStream, port: u16) -> Result<(), std::
// Check if this is one of our local accounts with a custom skin // Check if this is one of our local accounts with a custom skin
if let Some(json_body) = build_profile_response(uuid_str, port).await { if let Some(json_body) = build_profile_response(uuid_str, port).await {
let header = format!( return send_response(stream, "200 OK", "application/json; charset=utf-8", json_body.as_bytes()).await;
"HTTP/1.1 200 OK\r\nContent-Type: application/json; charset=utf-8\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
json_body.len(),
json_body
);
stream.write_all(header.as_bytes()).await?;
return Ok(());
} }
// For other players on the server, check Mojang session server so official players keep their skins // For other players on the server, check Mojang session server so official players keep their skins
if let Some(mojang_body) = fetch_mojang_profile(uuid_str).await { if let Some(mojang_body) = fetch_mojang_profile(uuid_str).await {
let header = format!( return send_response(stream, "200 OK", "application/json; charset=utf-8", mojang_body.as_bytes()).await;
"HTTP/1.1 200 OK\r\nContent-Type: application/json; charset=utf-8\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
mojang_body.len(),
mojang_body
);
stream.write_all(header.as_bytes()).await?;
return Ok(());
} }
// Offline / unknown player: return 204 No Content -> vanilla Minecraft renders default Steve/Alex // Offline / unknown player: return 204 No Content -> vanilla Minecraft renders default Steve/Alex
let response = "HTTP/1.1 204 No Content\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"; return send_no_content(stream).await;
stream.write_all(response.as_bytes()).await?;
return Ok(());
} }
// 5. Texture request: /textures/<id>.png // 5. Texture request: /textures/<id>.png or /texture/<id>
if let Some(texture_raw) = path.strip_prefix("/textures/") { let texture_opt = path
.strip_prefix("/textures/")
.or_else(|| path.strip_prefix("/texture/"));
if let Some(texture_raw) = texture_opt {
let texture_id = texture_raw.trim_matches('/').trim_end_matches(".png"); let texture_id = texture_raw.trim_matches('/').trim_end_matches(".png");
if let Some(png_bytes) = load_skin_png(texture_id).await { if let Some(png_bytes) = load_skin_png(texture_id).await {
let header = format!( let body_to_send = if method == "HEAD" { &[][..] } else { &png_bytes[..] };
"HTTP/1.1 200 OK\r\nContent-Type: image/png\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", return send_response(stream, "200 OK", "image/png", body_to_send).await;
png_bytes.len()
);
stream.write_all(header.as_bytes()).await?;
if method == "GET" {
stream.write_all(&png_bytes).await?;
}
return Ok(());
} }
} }
send_no_content(stream).await
}
async fn send_response(
mut stream: TcpStream,
status: &str,
content_type: &str,
body: &[u8],
) -> Result<(), std::io::Error> {
let header = format!(
"HTTP/1.1 {status}\r\nContent-Type: {content_type}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
body.len()
);
stream.write_all(header.as_bytes()).await?;
if !body.is_empty() {
stream.write_all(body).await?;
}
let _ = stream.flush().await;
let _ = stream.shutdown().await;
Ok(())
}
async fn send_no_content(mut stream: TcpStream) -> Result<(), std::io::Error> {
let response = "HTTP/1.1 204 No Content\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"; let response = "HTTP/1.1 204 No Content\r\nContent-Length: 0\r\nConnection: close\r\n\r\n";
stream.write_all(response.as_bytes()).await?; stream.write_all(response.as_bytes()).await?;
let _ = stream.flush().await;
let _ = stream.shutdown().await;
Ok(()) Ok(())
} }
@ -332,53 +320,32 @@ pub fn to_undashed_uuid(s: &str) -> String {
async fn find_skin_path(uuid_or_name: &str) -> Option<PathBuf> { async fn find_skin_path(uuid_or_name: &str) -> Option<PathBuf> {
let dashed = to_dashed_uuid(uuid_or_name); let dashed = to_dashed_uuid(uuid_or_name);
let undashed = to_undashed_uuid(uuid_or_name); let undashed = to_undashed_uuid(uuid_or_name);
let lower = uuid_or_name.to_lowercase();
if let Ok(p) = skin_file_path(&dashed) let mut keys = vec![dashed.clone(), undashed.clone(), lower.clone(), uuid_or_name.to_string()];
&& p.exists()
{
return Some(p);
}
if let Ok(p) = skin_file_path(&undashed)
&& p.exists()
{
return Some(p);
}
if let Ok(p) = skin_file_path(uuid_or_name)
&& p.exists()
{
return Some(p);
}
if let Some(accounts) = REGISTERED_ACCOUNTS.get() { if let Some(accounts) = REGISTERED_ACCOUNTS.get() {
let read = accounts.read(); let read = accounts.read();
if let Some(reg) = read if let Some(reg) = read
.get(&dashed) .get(&dashed)
.or_else(|| read.get(&undashed)) .or_else(|| read.get(&undashed))
.or_else(|| read.get(&uuid_or_name.to_lowercase())) .or_else(|| read.get(&lower))
{ {
if let Ok(p) = skin_file_path(&reg.uuid) keys.push(reg.uuid.clone());
&& p.exists() keys.push(reg.undashed_uuid.clone());
{ keys.push(reg.username.clone());
return Some(p); keys.push(reg.username.to_lowercase());
}
if let Ok(p) = skin_file_path(&reg.undashed_uuid)
&& p.exists()
{
return Some(p);
}
if let Ok(dir) = skins_dir() {
let name_path = dir.join(format!("{}.png", reg.username));
if name_path.exists() {
return Some(name_path);
}
}
} }
} }
if let Ok(dir) = skins_dir() { let search_dirs = oneclient_common::paths::all_skins_dirs();
let name_path = dir.join(format!("{uuid_or_name}.png"));
if name_path.exists() { for dir in &search_dirs {
return Some(name_path); for key in &keys {
let p1 = dir.join(format!("{key}.png"));
if p1.is_file() {
return Some(p1);
}
} }
} }
@ -401,17 +368,21 @@ async fn resolve_username(uuid_input: &str) -> String {
} }
} }
// Try reading from auth.json // Try reading from auth.json across all data directory candidates
if let Ok(auth_p) = oneclient_common::paths::auth_file() for skin_dir in oneclient_common::paths::all_skins_dirs() {
&& let Ok(content) = polyio::read_to_string(&auth_p).await if let Some(data_dir) = skin_dir.parent() {
&& let Ok(val) = serde_json::from_str::<serde_json::Value>(&content) let auth_p = data_dir.join("auth.json");
&& let Some(users) = val.get("users").and_then(|u| u.as_object()) if let Ok(content) = polyio::read_to_string(&auth_p).await
{ && let Ok(val) = serde_json::from_str::<serde_json::Value>(&content)
for (id, user) in users { && let Some(users) = val.get("users").and_then(|u| u.as_object())
if (id == &dashed || id.replace('-', "").to_lowercase() == undashed)
&& let Some(name) = user.get("username").and_then(|n| n.as_str())
{ {
return name.to_string(); for (id, user) in users {
if (id == &dashed || id.replace('-', "").to_lowercase() == undashed)
&& let Some(name) = user.get("username").and_then(|n| n.as_str())
{
return name.to_string();
}
}
} }
} }
} }
@ -423,13 +394,21 @@ async fn read_skin_metadata(uuid_or_name: &str) -> bool {
let dashed = to_dashed_uuid(uuid_or_name); let dashed = to_dashed_uuid(uuid_or_name);
let undashed = to_undashed_uuid(uuid_or_name); let undashed = to_undashed_uuid(uuid_or_name);
let lower = uuid_or_name.to_lowercase(); let lower = uuid_or_name.to_lowercase();
for key in [&dashed, &undashed, &lower] { let keys = [&dashed, &undashed, &lower, &uuid_or_name.to_string()];
if let Ok(meta_p) = skin_meta_path(key)
&& meta_p.exists() let search_dirs = oneclient_common::paths::all_skins_dirs();
&& let Ok(s) = polyio::read_to_string(&meta_p).await
&& let Ok(m) = serde_json::from_str::<SkinMetadata>(&s) for dir in &search_dirs {
{ for key in &keys {
return m.is_slim; for ext in ["json", "meta.json"] {
let meta_p = dir.join(format!("{key}.{ext}"));
if meta_p.is_file()
&& let Ok(s) = polyio::read_to_string(&meta_p).await
&& let Ok(m) = serde_json::from_str::<SkinMetadata>(&s)
{
return m.is_slim;
}
}
} }
} }
false false
@ -523,11 +502,11 @@ pub async fn build_user_properties(uuid_input: &str, port: u16) -> Option<String
let prop_json = if let Some(sig) = sign_sha1_with_rsa(encoded_textures.as_bytes()) { let prop_json = if let Some(sig) = sign_sha1_with_rsa(encoded_textures.as_bytes()) {
format!( format!(
r#"{{"textures":[{{"name":"textures","value":"{encoded_textures}","signature":"{sig}"}}]}}"# r#"{{"textures":[{{"value":"{encoded_textures}","signature":"{sig}"}}]}}"#
) )
} else { } else {
format!( format!(
r#"{{"textures":[{{"name":"textures","value":"{encoded_textures}"}}]}}"# r#"{{"textures":[{{"value":"{encoded_textures}"}}]}}"#
) )
}; };
@ -829,7 +808,7 @@ mod tests {
let test_user = "TestPlayer"; let test_user = "TestPlayer";
register_launch_account(test_uuid, test_user); register_launch_account(test_uuid, test_user);
if let Ok(dir) = skins_dir() { if let Ok(dir) = oneclient_common::paths::skins_dir() {
let _ = polyio::create_dir_all(&dir).await; let _ = polyio::create_dir_all(&dir).await;
let skin_p = dir.join(format!("{test_uuid}.png")); let skin_p = dir.join(format!("{test_uuid}.png"));
// 1x1 png or dummy png // 1x1 png or dummy png