Unlucky Launcher v0.0.1-alpha
CI / fmt (rustfmt --check) (push) Canceled after 0s
CI / clippy (ubuntu-latest) (push) Canceled after 0s
CI / clippy (windows-latest) (push) Canceled after 0s
CI / test (ubuntu-latest) (push) Canceled after 0s
CI / test (windows-latest) (push) Canceled after 0s
CI / build release (ubuntu-latest) (push) Canceled after 0s
CI / build release (windows-latest) (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
CI / fmt (rustfmt --check) (push) Canceled after 0s
CI / clippy (ubuntu-latest) (push) Canceled after 0s
CI / clippy (windows-latest) (push) Canceled after 0s
CI / test (ubuntu-latest) (push) Canceled after 0s
CI / test (windows-latest) (push) Canceled after 0s
CI / build release (ubuntu-latest) (push) Canceled after 0s
CI / build release (windows-latest) (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Rust 原生单文件 Minecraft 启动器:CLI 完整可用(install/launch/accounts/java/diagnose/agent), MCP server 接入外部 Agent 工具,GUI 开发中。
This commit is contained in:
commit
bc41dfcf7d
517 files changed
+92844
No files matched your search
@@ -0,0 +1,86 @@
|
||||
//! 认证服务器元数据里我们真正需要的字段(服务发现的产物)。
|
||||
//!
|
||||
//! authlib-injector 规范里根路径 `/` 返回 `{ meta, skinDomains, signaturePublickey }`
|
||||
//! (`analyse/04` §6.3)。本期**只保存**名字/非邮箱登录/skinDomains:
|
||||
//! `signaturePublickey` 与 `prefetched` 的 Base64 属于注入参数(后续 mission),
|
||||
//! 现在存了也没人消费,不如不存。
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
/// 服务器元数据(缺失字段一律回落默认值)。
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AuthlibServerMeta {
|
||||
/// 服务器显示名(缺省回落 URL)。
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub name: Option<String>,
|
||||
/// 是否允许非邮箱登录名(决定输入框校验)。
|
||||
#[serde(default)]
|
||||
pub non_email_login: bool,
|
||||
/// `skinDomains`(纹理白名单,注入参数会用;本期只保存)。
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub skin_domains: Vec<String>,
|
||||
}
|
||||
|
||||
impl AuthlibServerMeta {
|
||||
/// 从根元数据 JSON 解析(容错:缺字段就用默认值)。
|
||||
#[must_use]
|
||||
pub fn from_value(value: &Value) -> Self {
|
||||
let meta = value.get("meta");
|
||||
let name = meta
|
||||
.and_then(|meta| meta.get("serverName"))
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_string);
|
||||
let non_email_login = meta
|
||||
.and_then(|meta| meta.get("feature.non_email_login"))
|
||||
.and_then(Value::as_bool)
|
||||
.unwrap_or(false);
|
||||
let skin_domains = value
|
||||
.get("skinDomains")
|
||||
.and_then(Value::as_array)
|
||||
.map(|items| {
|
||||
items
|
||||
.iter()
|
||||
.filter_map(Value::as_str)
|
||||
.map(str::to_string)
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
Self {
|
||||
name,
|
||||
non_email_login,
|
||||
skin_domains,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::AuthlibServerMeta;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn parses_full_metadata() {
|
||||
let value = json!({
|
||||
"meta": {
|
||||
"serverName": "Example Skin Site",
|
||||
"feature.non_email_login": true
|
||||
},
|
||||
"skinDomains": ["example.com", "*.example.com"],
|
||||
"signaturePublickey": "-----BEGIN PUBLIC KEY-----"
|
||||
});
|
||||
let meta = AuthlibServerMeta::from_value(&value);
|
||||
assert_eq!(meta.name.as_deref(), Some("Example Skin Site"));
|
||||
assert!(meta.non_email_login);
|
||||
assert_eq!(meta.skin_domains.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tolerates_missing_fields() {
|
||||
let meta = AuthlibServerMeta::from_value(&json!({}));
|
||||
assert_eq!(meta, AuthlibServerMeta::default());
|
||||
assert!(meta.name.is_none());
|
||||
assert!(!meta.non_email_login);
|
||||
assert!(meta.skin_domains.is_empty());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
//! authlib 轨的纯函数/模型测试(HTTP 部分在 `tests/auth/authlib_api.rs`)。
|
||||
|
||||
use super::{normalize_api_root, AuthlibEndpoints, AuthlibProfile, AuthlibSession, USER_TYPE_MSA};
|
||||
use crate::auth::secret::Secret;
|
||||
|
||||
#[test]
|
||||
fn api_root_normalization() {
|
||||
assert_eq!(
|
||||
normalize_api_root("littleskin.cn/api/yggdrasil").expect("补协议"),
|
||||
"https://littleskin.cn/api/yggdrasil/"
|
||||
);
|
||||
assert_eq!(
|
||||
normalize_api_root(" http://127.0.0.1:8080/api/yggdrasil/ ").expect("保留协议"),
|
||||
"http://127.0.0.1:8080/api/yggdrasil/"
|
||||
);
|
||||
assert_eq!(
|
||||
normalize_api_root("https://littleskin.cn").expect("根域名"),
|
||||
"https://littleskin.cn/"
|
||||
);
|
||||
assert!(normalize_api_root(" ").is_err());
|
||||
assert!(normalize_api_root("https://").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn endpoints_follow_directory_layout() {
|
||||
let endpoints =
|
||||
AuthlibEndpoints::from_api_root("https://example.invalid/api/yggdrasil").expect("端点");
|
||||
assert_eq!(
|
||||
endpoints.authenticate,
|
||||
"https://example.invalid/api/yggdrasil/authserver/authenticate"
|
||||
);
|
||||
assert_eq!(
|
||||
endpoints.refresh,
|
||||
"https://example.invalid/api/yggdrasil/authserver/refresh"
|
||||
);
|
||||
assert_eq!(
|
||||
endpoints.validate,
|
||||
"https://example.invalid/api/yggdrasil/authserver/validate"
|
||||
);
|
||||
assert_eq!(
|
||||
endpoints.invalidate,
|
||||
"https://example.invalid/api/yggdrasil/authserver/invalidate"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn placeholders_require_a_profile() {
|
||||
let without = AuthlibSession {
|
||||
access_token: Secret::new("token"),
|
||||
client_token: Secret::new("client"),
|
||||
selected_profile: None,
|
||||
};
|
||||
assert!(!without.has_profile());
|
||||
assert!(without.to_placeholders().is_err());
|
||||
|
||||
let with = AuthlibSession {
|
||||
selected_profile: Some(AuthlibProfile {
|
||||
id: "0123456789abcdef0123456789abcdef".to_string(),
|
||||
name: "Steve".to_string(),
|
||||
}),
|
||||
..without
|
||||
};
|
||||
assert!(with.has_profile());
|
||||
let placeholders = with.to_placeholders().expect("占位符");
|
||||
assert_eq!(placeholders.player_name, "Steve");
|
||||
assert_eq!(placeholders.uuid, "0123456789abcdef0123456789abcdef");
|
||||
assert_eq!(placeholders.user_type, USER_TYPE_MSA);
|
||||
assert_eq!(placeholders.access_token, "token");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_debug_never_leaks_tokens() {
|
||||
let session = AuthlibSession {
|
||||
access_token: Secret::new("access-token-value"),
|
||||
client_token: Secret::new("client-token-value"),
|
||||
selected_profile: None,
|
||||
};
|
||||
let debug = format!("{session:?}");
|
||||
assert!(!debug.contains("access-token-value"));
|
||||
assert!(!debug.contains("client-token-value"));
|
||||
assert_eq!(session.secrets().len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_survives_json_roundtrip() {
|
||||
let session = AuthlibSession {
|
||||
access_token: Secret::new("access-token-value"),
|
||||
client_token: Secret::new("client-token-value"),
|
||||
selected_profile: Some(AuthlibProfile {
|
||||
id: "0123456789abcdef0123456789abcdef".to_string(),
|
||||
name: "Steve".to_string(),
|
||||
}),
|
||||
};
|
||||
let text = serde_json::to_string(&session).expect("序列化");
|
||||
let back: AuthlibSession = serde_json::from_str(&text).expect("反序列化");
|
||||
assert_eq!(back, session);
|
||||
}
|
||||
Reference in new issue
Block a user