scl_core/auth/
authlib.rs

1//! 用于 authlib-injector 第三方登录的登录逻辑
2
3use std::str::FromStr;
4
5use anyhow::Context;
6use base64::prelude::*;
7use surf::StatusCode;
8
9use crate::{
10    auth::structs::{mojang::*, AuthMethod},
11    http::RequestResult,
12    password::Password,
13    prelude::*,
14};
15
16#[derive(Debug, Default, Deserialize)]
17#[serde(default)]
18struct ServerMetaLinks {
19    pub homepage: String,
20}
21
22#[derive(Debug, Default, Deserialize)]
23#[serde(default)]
24#[serde(rename_all = "camelCase")]
25struct ServerMeta {
26    pub server_name: String,
27    pub links: Option<ServerMetaLinks>,
28}
29
30#[derive(Debug, Default, Deserialize)]
31struct APIMetaData {
32    pub meta: ServerMeta,
33}
34
35#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)]
36#[serde(rename_all = "camelCase")]
37pub(crate) struct RefreshBody {
38    pub access_token: Password,
39    #[serde(skip_serializing_if = "String::is_empty")]
40    pub client_token: String,
41    pub request_user: bool,
42    pub selected_profile: Option<AvaliableProfile>,
43}
44
45async fn get_head_skin(api_location: &str, uuid: &str) -> DynResult<(Vec<u8>, Vec<u8>)> {
46    let uri = format!("{api_location}sessionserver/session/minecraft/profile/{uuid}");
47    let result: ProfileResponse = crate::http::no_retry::get(&uri)
48        .await
49        .map_err(|e| anyhow::anyhow!("发送获取皮肤请求到 {} 时发生错误:{:?}", uri, e))?
50        .body_json()
51        .await
52        .map_err(|e| anyhow::anyhow!("接收获取皮肤响应到 {} 时发生错误:{:?}", uri, e))?;
53    if let Some(prop) = result
54        .properties
55        .iter()
56        .find(|a| a.name.as_str() == "textures")
57    {
58        let texture_raw = &prop.value;
59        let texture_raw = BASE64_STANDARD.decode(texture_raw)?;
60        let texture_data: ProfileTexture = serde_json::from_slice(&texture_raw)?;
61        if let Some(textures) = texture_data.textures {
62            if let Some(skin) = textures.skin {
63                let skin_url = skin.url;
64                crate::auth::parse_head_skin(
65                    crate::http::no_retry::get(skin_url)
66                        .recv_bytes()
67                        .await
68                        .map_err(|e| anyhow::anyhow!(e))?,
69                )
70            } else {
71                Ok(Default::default())
72            }
73        } else {
74            Ok(Default::default())
75        }
76    } else {
77        Ok(Default::default())
78    }
79}
80
81/// 根据初次登陆/二次验证取得的用户令牌,刷新验证出可供正常游戏的登录令牌
82///
83/// 详情参考[启动器技术规范](https://github.com/yushijinhun/authlib-injector/wiki/Yggdrasil-%E6%9C%8D%E5%8A%A1%E7%AB%AF%E6%8A%80%E6%9C%AF%E8%A7%84%E8%8C%83#%E5%88%B7%E6%96%B0)
84pub async fn refresh_token(
85    auth_method: AuthMethod,
86    client_token: &str,
87    provide_selected_profile: bool,
88) -> DynResult<AuthMethod> {
89    if let AuthMethod::AuthlibInjector {
90        api_location,
91        server_name,
92        server_homepage,
93        server_meta,
94        access_token,
95        uuid,
96        player_name,
97        ..
98    } = auth_method
99    {
100        let res: RequestResult<AuthenticateResponse> = dbg!(crate::http::no_retry::post_data(
101            dbg!(&format!("{api_location}authserver/refresh")),
102            dbg!(&RefreshBody {
103                access_token: access_token.to_owned(),
104                client_token: client_token.to_owned(),
105                request_user: provide_selected_profile,
106                selected_profile: if provide_selected_profile {
107                    Some(AvaliableProfile {
108                        name: player_name.to_owned(),
109                        id: uuid.to_owned(),
110                    })
111                } else {
112                    None
113                },
114            }),
115        )
116        .await
117        .context("无法请求刷新令牌接口")?);
118
119        match res {
120            RequestResult::Ok(res) => {
121                let selected_profile = res.selected_profile.unwrap_or_else(|| AvaliableProfile {
122                    name: player_name.to_owned(),
123                    id: uuid.to_owned(),
124                });
125
126                let (head_skin, hat_skin) =
127                    get_head_skin(&api_location, &selected_profile.id).await?;
128
129                let refreshed_method = AuthMethod::AuthlibInjector {
130                    api_location: api_location.to_owned(),
131                    server_name: server_name.to_owned(),
132                    server_homepage,
133                    server_meta,
134                    access_token: res.access_token,
135                    uuid: selected_profile.id,
136                    player_name: selected_profile.name,
137                    head_skin,
138                    hat_skin,
139                };
140
141                Ok(refreshed_method)
142            }
143            RequestResult::Err(a) => {
144                if a.error_message.is_empty() {
145                    match a.error.as_str() {
146                        "ForbiddenOperationException" => anyhow::bail!("未授权的访问"),
147                        "IllegalArgumentException" => anyhow::bail!("非法令牌绑定"),
148                        _ => anyhow::bail!("未知原因:{}", a.error),
149                    }
150                } else {
151                    anyhow::bail!("{}:{}", a.error, a.error_message)
152                }
153            }
154        }
155    } else {
156        anyhow::bail!("此函数只支持 Authlib Injector 第三方登录")
157    }
158}
159
160/// 使用指定的 Authlib 服务器地址和对应的账户密码开始进行 Authlib 第三方登录验证
161///
162/// 根据[启动器技术规范](https://github.com/yushijinhun/authlib-injector/wiki/%E5%90%AF%E5%8A%A8%E5%99%A8%E6%8A%80%E6%9C%AF%E8%A7%84%E8%8C%83)编写
163///
164/// 如果验证成功,则会返回这个账户旗下所有角色。
165/// 如果用户名和角色名称一致,则只会返回那个角色。
166pub async fn start_auth(
167    _ctx: Option<impl Reporter>,
168    authlib_host: &str,
169    username: String,
170    password: Password,
171    client_token: &str,
172) -> DynResult<Vec<AuthMethod>> {
173    // 找到 API 地址,使用 ALI
174    let api_location = {
175        let a = crate::http::get(authlib_host)
176            .await
177            .map_err(|_| anyhow::anyhow!("无法请求 Authlib API 服务器:{}", authlib_host))?;
178        if let Some(h) = a.header("X-Authlib-Injector-API-Location") {
179            h.last().to_string()
180        } else {
181            authlib_host.to_owned()
182        }
183    };
184
185    // 处理链接格式
186    let api_location = {
187        if api_location.starts_with("http") {
188            url::Url::parse(&api_location)?
189        } else {
190            url::Url::parse(authlib_host)?.join(&api_location)?
191        }
192    };
193
194    let api_location = api_location.to_string();
195    let api_location = if api_location.ends_with('/') {
196        api_location
197    } else {
198        format!("{api_location}/")
199    };
200    let api_location_url = url::Url::from_str(&api_location)?;
201
202    let meta_res: RequestResult<APIMetaData> = crate::http::no_retry::get_data(&api_location)
203        .await
204        .map_err(|e| anyhow::anyhow!("无法接收 Authlib 服务器元数据响应:{:?}", e))?;
205
206    let (server_name, server_homepage) = if let RequestResult::Ok(meta) = meta_res {
207        let mut result = (String::new(), String::new());
208        if meta.meta.server_name.is_empty() {
209            result.0 = url::Url::from_str(&api_location)?
210                .host()
211                .ok_or_else(|| anyhow::anyhow!("无法取得 Authlib 服务器接口的 Host 部分"))?
212                .to_string();
213        } else {
214            result.0 = meta.meta.server_name;
215        }
216        if let Some(server_homepage) = meta.meta.links.map(|a| a.homepage) {
217            result.1 = server_homepage;
218        } else {
219            result.1 = api_location_url.origin().ascii_serialization();
220        }
221        result
222    } else {
223        (
224            api_location_url
225                .host()
226                .ok_or_else(|| anyhow::anyhow!("无法取得 Authlib 服务器接口的 Host 部分"))?
227                .to_string(),
228            api_location_url.origin().ascii_serialization(),
229        )
230    };
231
232    let server_meta = crate::http::no_retry::get(&api_location)
233        .recv_bytes()
234        .await
235        .map_err(|e| anyhow::anyhow!("无法接收登录接口元数据:{:?}", e))?;
236    let server_meta = BASE64_STANDARD.encode(server_meta);
237
238    // 登录链接
239    let auth_url = format!("{api_location}authserver/authenticate");
240    let auth_body = AuthenticateBody {
241        username: username.to_owned(),
242        password,
243        client_token: client_token.to_owned(),
244        ..Default::default()
245    };
246    let resp: RequestResult<AuthenticateResponse> =
247        crate::http::no_retry::post_data(&auth_url, &auth_body)
248            .await
249            .map_err(|e| anyhow::anyhow!("无法解析登录接口回调:{} {:?}", auth_url, e))?;
250
251    match resp {
252        RequestResult::Ok(a) => {
253            if let Some(selected_profile) = a.selected_profile {
254                if selected_profile.name == username {
255                    let (head_skin, hat_skin) =
256                        get_head_skin(&api_location, &selected_profile.id).await?;
257                    return Ok(vec![AuthMethod::AuthlibInjector {
258                        api_location,
259                        server_name,
260                        server_homepage,
261                        server_meta,
262                        access_token: a.access_token,
263                        uuid: selected_profile.id,
264                        player_name: selected_profile.name,
265                        head_skin,
266                        hat_skin,
267                    }]);
268                }
269            }
270            if !a.available_profiles.is_empty() {
271                if let Some(profile) = a.available_profiles.iter().find(|x| x.name == username) {
272                    let (head_skin, hat_skin) = get_head_skin(&api_location, &profile.id).await?;
273                    return Ok(vec![AuthMethod::AuthlibInjector {
274                        api_location,
275                        server_name,
276                        server_homepage,
277                        server_meta,
278                        access_token: a.access_token,
279                        uuid: profile.id.to_owned(),
280                        player_name: profile.name.to_owned(),
281                        head_skin,
282                        hat_skin,
283                    }]);
284                }
285
286                let skins_threads =
287                    futures::future::join_all(a.available_profiles.into_iter().map(|x| async {
288                        let (head_skin, hat_skin) = get_head_skin(&api_location, &x.id)
289                            .await
290                            .unwrap_or_else(|_| (vec![0; 2 * 4 * 64], vec![0; 2 * 4 * 64]));
291                        AuthMethod::AuthlibInjector {
292                            api_location: api_location.to_owned(),
293                            server_name: server_name.to_owned(),
294                            server_homepage: server_homepage.to_owned(),
295                            server_meta: server_meta.to_owned(),
296                            access_token: a.access_token.to_owned(),
297                            uuid: x.id,
298                            player_name: x.name,
299                            head_skin,
300                            hat_skin,
301                        }
302                    }))
303                    .await;
304                Ok(skins_threads)
305            } else {
306                anyhow::bail!("该账户没有可用的角色!")
307            }
308        }
309        RequestResult::Err(a) => {
310            if a.error_message.is_empty() {
311                match a.error.as_str() {
312                    "ForbiddenOperationException" => anyhow::bail!("未授权的访问"),
313                    "IllegalArgumentException" => anyhow::bail!("非法令牌绑定"),
314                    _ => anyhow::bail!("未知原因:{}", a.error),
315                }
316            } else {
317                anyhow::bail!("{}:{}", a.error, a.error_message)
318            }
319        }
320    }
321}
322
323/// 验证对应的访问令牌和当前启动器令牌是否可以用于现在进行游戏
324pub async fn validate(
325    api_location: &str,
326    access_token: &str,
327    client_token: &str,
328) -> DynResult<bool> {
329    let post_url = url::Url::parse(api_location)?.join("authserver/validate")?;
330    let resp = crate::http::post(post_url)
331        .body_json(&ValidateResponse {
332            access_token: access_token.into(),
333            client_token: client_token.to_owned(),
334        })
335        .map_err(|_| anyhow::anyhow!("无法序列化请求"))?
336        .await
337        .map_err(|_| anyhow::anyhow!("无法请求 Authlib API 服务器:{}", api_location))?;
338    Ok(resp.status() == StatusCode::NoContent)
339}