use std::str::FromStr;
use anyhow::Context;
use base64::prelude::*;
use surf::StatusCode;
use crate::{
auth::structs::{mojang::*, AuthMethod},
http::RequestResult,
password::Password,
prelude::*,
};
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
struct ServerMetaLinks {
pub homepage: String,
}
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
#[serde(rename_all = "camelCase")]
struct ServerMeta {
pub server_name: String,
pub links: Option<ServerMetaLinks>,
}
#[derive(Debug, Default, Deserialize)]
struct APIMetaData {
pub meta: ServerMeta,
}
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub(crate) struct RefreshBody {
pub access_token: Password,
#[serde(skip_serializing_if = "String::is_empty")]
pub client_token: String,
pub request_user: bool,
pub selected_profile: Option<AvaliableProfile>,
}
async fn get_head_skin(api_location: &str, uuid: &str) -> DynResult<(Vec<u8>, Vec<u8>)> {
let uri = format!("{api_location}sessionserver/session/minecraft/profile/{uuid}");
let result: ProfileResponse = crate::http::no_retry::get(&uri)
.await
.map_err(|e| anyhow::anyhow!("发送获取皮肤请求到 {} 时发生错误:{:?}", uri, e))?
.body_json()
.await
.map_err(|e| anyhow::anyhow!("接收获取皮肤响应到 {} 时发生错误:{:?}", uri, e))?;
if let Some(prop) = result
.properties
.iter()
.find(|a| a.name.as_str() == "textures")
{
let texture_raw = &prop.value;
let texture_raw = BASE64_STANDARD.decode(texture_raw)?;
let texture_data: ProfileTexture = serde_json::from_slice(&texture_raw)?;
if let Some(textures) = texture_data.textures {
if let Some(skin) = textures.skin {
let skin_url = skin.url;
crate::auth::parse_head_skin(
crate::http::no_retry::get(skin_url)
.recv_bytes()
.await
.map_err(|e| anyhow::anyhow!(e))?,
)
} else {
Ok(Default::default())
}
} else {
Ok(Default::default())
}
} else {
Ok(Default::default())
}
}
pub async fn refresh_token(
auth_method: AuthMethod,
client_token: &str,
provide_selected_profile: bool,
) -> DynResult<AuthMethod> {
if let AuthMethod::AuthlibInjector {
api_location,
server_name,
server_homepage,
server_meta,
access_token,
uuid,
player_name,
..
} = auth_method
{
let res: RequestResult<AuthenticateResponse> = crate::http::no_retry::post_data(
&format!("{api_location}authserver/refresh"),
&RefreshBody {
access_token: access_token.to_owned(),
client_token: client_token.to_owned(),
request_user: provide_selected_profile,
selected_profile: if provide_selected_profile {
Some(AvaliableProfile {
name: player_name.to_owned(),
id: uuid.to_owned(),
})
} else {
None
},
},
)
.await
.context("无法请求刷新令牌接口")?;
match res {
RequestResult::Ok(res) => {
let selected_profile = res.selected_profile.unwrap_or_else(|| AvaliableProfile {
name: player_name.to_owned(),
id: uuid.to_owned(),
});
let (head_skin, hat_skin) =
get_head_skin(&api_location, &selected_profile.id).await?;
let refreshed_method = AuthMethod::AuthlibInjector {
api_location: api_location.to_owned(),
server_name: server_name.to_owned(),
server_homepage,
server_meta,
access_token: res.access_token,
uuid: selected_profile.id,
player_name: selected_profile.name,
head_skin,
hat_skin,
};
Ok(refreshed_method)
}
RequestResult::Err(a) => {
if a.error_message.is_empty() {
match a.error.as_str() {
"ForbiddenOperationException" => anyhow::bail!("未授权的访问"),
"IllegalArgumentException" => anyhow::bail!("非法令牌绑定"),
_ => anyhow::bail!("未知原因:{}", a.error),
}
} else {
anyhow::bail!("{}:{}", a.error, a.error_message)
}
}
}
} else {
anyhow::bail!("此函数只支持 Authlib Injector 第三方登录")
}
}
pub async fn start_auth(
_ctx: Option<impl Reporter>,
authlib_host: &str,
username: String,
password: Password,
client_token: &str,
) -> DynResult<Vec<AuthMethod>> {
let api_location = {
let a = crate::http::get(authlib_host)
.await
.map_err(|_| anyhow::anyhow!("无法请求 Authlib API 服务器:{}", authlib_host))?;
if let Some(h) = a.header("X-Authlib-Injector-API-Location") {
h.last().to_string()
} else {
authlib_host.to_owned()
}
};
let api_location = {
if api_location.starts_with("http") {
url::Url::parse(&api_location)?
} else {
url::Url::parse(authlib_host)?.join(&api_location)?
}
};
let api_location = api_location.to_string();
let api_location = if api_location.ends_with('/') {
api_location
} else {
format!("{api_location}/")
};
let api_location_url = url::Url::from_str(&api_location)?;
let meta_res: RequestResult<APIMetaData> = crate::http::no_retry::get_data(&api_location)
.await
.map_err(|e| anyhow::anyhow!("无法接收 Authlib 服务器元数据响应:{:?}", e))?;
let (server_name, server_homepage) = if let RequestResult::Ok(meta) = meta_res {
let mut result = (String::new(), String::new());
if meta.meta.server_name.is_empty() {
result.0 = url::Url::from_str(&api_location)?
.host()
.ok_or_else(|| anyhow::anyhow!("无法取得 Authlib 服务器接口的 Host 部分"))?
.to_string();
} else {
result.0 = meta.meta.server_name;
}
if let Some(server_homepage) = meta.meta.links.map(|a| a.homepage) {
result.1 = server_homepage;
} else {
result.1 = api_location_url.origin().ascii_serialization();
}
result
} else {
(
api_location_url
.host()
.ok_or_else(|| anyhow::anyhow!("无法取得 Authlib 服务器接口的 Host 部分"))?
.to_string(),
api_location_url.origin().ascii_serialization(),
)
};
let server_meta = crate::http::no_retry::get(&api_location)
.recv_bytes()
.await
.map_err(|e| anyhow::anyhow!("无法接收登录接口元数据:{:?}", e))?;
let server_meta = BASE64_STANDARD.encode(server_meta);
let auth_url = format!("{api_location}authserver/authenticate");
let auth_body = AuthenticateBody {
username: username.to_owned(),
password,
client_token: client_token.to_owned(),
..Default::default()
};
let resp: RequestResult<AuthenticateResponse> =
crate::http::no_retry::post_data(&auth_url, &auth_body)
.await
.map_err(|e| anyhow::anyhow!("无法解析登录接口回调:{} {:?}", auth_url, e))?;
match resp {
RequestResult::Ok(a) => {
if let Some(selected_profile) = a.selected_profile {
if selected_profile.name == username {
let (head_skin, hat_skin) =
get_head_skin(&api_location, &selected_profile.id).await?;
return Ok(vec![AuthMethod::AuthlibInjector {
api_location,
server_name,
server_homepage,
server_meta,
access_token: a.access_token,
uuid: selected_profile.id,
player_name: selected_profile.name,
head_skin,
hat_skin,
}]);
}
}
if !a.available_profiles.is_empty() {
if let Some(profile) = a.available_profiles.iter().find(|x| x.name == username) {
let (head_skin, hat_skin) = get_head_skin(&api_location, &profile.id).await?;
return Ok(vec![AuthMethod::AuthlibInjector {
api_location,
server_name,
server_homepage,
server_meta,
access_token: a.access_token,
uuid: profile.id.to_owned(),
player_name: profile.name.to_owned(),
head_skin,
hat_skin,
}]);
}
let skins_threads =
futures::future::join_all(a.available_profiles.into_iter().map(|x| async {
let (head_skin, hat_skin) = get_head_skin(&api_location, &x.id)
.await
.unwrap_or_else(|_| (vec![0; 2 * 4 * 64], vec![0; 2 * 4 * 64]));
AuthMethod::AuthlibInjector {
api_location: api_location.to_owned(),
server_name: server_name.to_owned(),
server_homepage: server_homepage.to_owned(),
server_meta: server_meta.to_owned(),
access_token: a.access_token.to_owned(),
uuid: x.id,
player_name: x.name,
head_skin,
hat_skin,
}
}))
.await;
Ok(skins_threads)
} else {
anyhow::bail!("该账户没有可用的角色!")
}
}
RequestResult::Err(a) => {
if a.error_message.is_empty() {
match a.error.as_str() {
"ForbiddenOperationException" => anyhow::bail!("未授权的访问"),
"IllegalArgumentException" => anyhow::bail!("非法令牌绑定"),
_ => anyhow::bail!("未知原因:{}", a.error),
}
} else {
anyhow::bail!("{}:{}", a.error, a.error_message)
}
}
}
}
pub async fn validate(
api_location: &str,
access_token: &str,
client_token: &str,
) -> DynResult<bool> {
let post_url = url::Url::parse(api_location)?.join("authserver/validate")?;
let resp = crate::http::post(post_url)
.body_json(&ValidateResponse {
access_token: access_token.into(),
client_token: client_token.to_owned(),
})
.map_err(|_| anyhow::anyhow!("无法序列化请求"))?
.await
.map_err(|_| anyhow::anyhow!("无法请求 Authlib API 服务器:{}", api_location))?;
Ok(resp.status() == StatusCode::NoContent)
}