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.
# Windows has 1MB, it isn't enough, so it's changed to 8MB also.
[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"]}
[profile.release]
codegen-units = 1
debug = "line-tables-only"
codegen-units = 16
debug = false
lto = "thin"
opt-level = 3
panic = "abort"
@ -174,5 +174,9 @@ lto = false
strip = false
[profile.dev]
opt-level = 0
debug = 0
strip = "debuginfo"
[profile.dev.package."*"]
opt-level = 2

View file

@ -150,9 +150,6 @@ pub async fn save_account_skin(
.await
.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)
.map_err(|e| format!("Invalid image format: {e}"))?
.to_rgba8();
@ -177,17 +174,20 @@ pub async fn save_account_skin(
.write_to(&mut cursor, image::ImageFormat::Png)
.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_json = serde_json::to_string_pretty(&meta)
.map_err(|e| format!("Failed to serialize metadata: {e}"))?;
polyio::write(&meta_path, meta_json.as_bytes())
.await
.map_err(|e| format!("Failed to write skin metadata: {e}"))?;
for dir in oneclient_common::paths::all_skins_dirs() {
let _ = polyio::create_dir_all(&dir).await;
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()) {
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> {
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())?;
if skin_path.exists() {
let _ = polyio::remove_file(&skin_path).await;
}
if meta_path.exists() {
let _ = polyio::remove_file(&meta_path).await;
for dir in oneclient_common::paths::all_skins_dirs() {
let _ = polyio::remove_file(&dir.join(format!("{uuid}.png"))).await;
let _ = polyio::remove_file(&dir.join(format!("{uuid}.json"))).await;
let _ = polyio::remove_file(&dir.join(format!("{uuid}.meta.json"))).await;
if let Some(uname) = username.filter(|u| !u.is_empty()) {
let _ = polyio::remove_file(&dir.join(format!("{uname}.png"))).await;
let _ = polyio::remove_file(&dir.join(format!("{uname}.json"))).await;
}
}
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"))
}
/// 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> {
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)?;
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
&& version.id == "1.21.11"
{
@ -423,12 +430,18 @@ async fn start(
&& let Some(user_props) =
crate::game::skin_server::build_user_properties(&account.id.to_string(), port).await
{
let mut replaced = false;
for i in 0..mc_args.len() {
if mc_args[i] == "--userProperties" && i + 1 < mc_args.len() && mc_args[i + 1] == "{}" {
mc_args[i + 1] = user_props;
if mc_args[i] == "--userProperties" && i + 1 < mc_args.len() {
mc_args[i + 1] = user_props.clone();
replaced = true;
break;
}
}
if !replaced {
mc_args.push("--userProperties".to_string());
mc_args.push(user_props);
}
}
arguments::append_profile_game_arguments(
&mut mc_args,

View file

@ -5,7 +5,7 @@ use std::time::{Duration, Instant};
use base64::Engine;
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 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");
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";
stream.write_all(response.as_bytes()).await?;
return Ok(());
return send_no_content(stream).await;
}
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",
"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
}).to_string();
let header = format!(
"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(());
return send_response(stream, "200 OK", "application/json; charset=utf-8", body.as_bytes()).await;
}
// 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}],
"playerCertificateKeys": [{"publicKey": key_b64}]
}).to_string();
let header = format!(
"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(());
return send_response(stream, "200 OK", "application/json; charset=utf-8", body.as_bytes()).await;
}
// 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 header = format!(
"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(());
return send_response(stream, "200 OK", "application/json; charset=utf-8", body.as_bytes()).await;
}
// 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
if let Some(json_body) = build_profile_response(uuid_str, port).await {
let header = format!(
"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(());
return send_response(stream, "200 OK", "application/json; charset=utf-8", json_body.as_bytes()).await;
}
// 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 {
let header = format!(
"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(());
return send_response(stream, "200 OK", "application/json; charset=utf-8", mojang_body.as_bytes()).await;
}
// 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";
stream.write_all(response.as_bytes()).await?;
return Ok(());
return send_no_content(stream).await;
}
// 5. Texture request: /textures/<id>.png
if let Some(texture_raw) = path.strip_prefix("/textures/") {
// 5. Texture request: /textures/<id>.png or /texture/<id>
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");
if let Some(png_bytes) = load_skin_png(texture_id).await {
let header = format!(
"HTTP/1.1 200 OK\r\nContent-Type: image/png\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
png_bytes.len()
);
stream.write_all(header.as_bytes()).await?;
if method == "GET" {
stream.write_all(&png_bytes).await?;
}
return Ok(());
let body_to_send = if method == "HEAD" { &[][..] } else { &png_bytes[..] };
return send_response(stream, "200 OK", "image/png", body_to_send).await;
}
}
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";
stream.write_all(response.as_bytes()).await?;
let _ = stream.flush().await;
let _ = stream.shutdown().await;
Ok(())
}
@ -332,53 +320,32 @@ pub fn to_undashed_uuid(s: &str) -> String {
async fn find_skin_path(uuid_or_name: &str) -> Option<PathBuf> {
let dashed = to_dashed_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)
&& 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);
}
let mut keys = vec![dashed.clone(), undashed.clone(), lower.clone(), uuid_or_name.to_string()];
if let Some(accounts) = REGISTERED_ACCOUNTS.get() {
let read = accounts.read();
if let Some(reg) = read
.get(&dashed)
.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)
&& p.exists()
{
return Some(p);
}
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);
}
}
keys.push(reg.uuid.clone());
keys.push(reg.undashed_uuid.clone());
keys.push(reg.username.clone());
keys.push(reg.username.to_lowercase());
}
}
if let Ok(dir) = skins_dir() {
let name_path = dir.join(format!("{uuid_or_name}.png"));
if name_path.exists() {
return Some(name_path);
let search_dirs = oneclient_common::paths::all_skins_dirs();
for dir in &search_dirs {
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
if let Ok(auth_p) = oneclient_common::paths::auth_file()
&& let Ok(content) = polyio::read_to_string(&auth_p).await
&& let Ok(val) = serde_json::from_str::<serde_json::Value>(&content)
&& let Some(users) = val.get("users").and_then(|u| u.as_object())
{
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())
// Try reading from auth.json across all data directory candidates
for skin_dir in oneclient_common::paths::all_skins_dirs() {
if let Some(data_dir) = skin_dir.parent() {
let auth_p = data_dir.join("auth.json");
if let Ok(content) = polyio::read_to_string(&auth_p).await
&& let Ok(val) = serde_json::from_str::<serde_json::Value>(&content)
&& let Some(users) = val.get("users").and_then(|u| u.as_object())
{
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 undashed = to_undashed_uuid(uuid_or_name);
let lower = uuid_or_name.to_lowercase();
for key in [&dashed, &undashed, &lower] {
if let Ok(meta_p) = skin_meta_path(key)
&& meta_p.exists()
&& let Ok(s) = polyio::read_to_string(&meta_p).await
&& let Ok(m) = serde_json::from_str::<SkinMetadata>(&s)
{
return m.is_slim;
let keys = [&dashed, &undashed, &lower, &uuid_or_name.to_string()];
let search_dirs = oneclient_common::paths::all_skins_dirs();
for dir in &search_dirs {
for key in &keys {
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
@ -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()) {
format!(
r#"{{"textures":[{{"name":"textures","value":"{encoded_textures}","signature":"{sig}"}}]}}"#
r#"{{"textures":[{{"value":"{encoded_textures}","signature":"{sig}"}}]}}"#
)
} else {
format!(
r#"{{"textures":[{{"name":"textures","value":"{encoded_textures}"}}]}}"#
r#"{{"textures":[{{"value":"{encoded_textures}"}}]}}"#
)
};
@ -829,7 +808,7 @@ mod tests {
let test_user = "TestPlayer";
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 skin_p = dir.join(format!("{test_uuid}.png"));
// 1x1 png or dummy png