Files
Starlight_Launcher/packages/app-lib/src/api/minecraft_auth.rs
T
Disy 51fb0c30f7
Axolotl desktop CI / guardrails (push) Has been cancelled
Repository checks / typos (push) Has been cancelled
Repository checks / tombi (push) Has been cancelled
Rust checks / shear (push) Has been cancelled
Sync source to CNB / Sync Git ref (push) Has been cancelled
Axolotl desktop CI / desktop (macos-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (ubuntu-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (windows-latest) (push) Has been cancelled
Axolotl desktop CI / website (push) Has been cancelled
Sync LobeHub models / sync (push) Has been cancelled
fix: 完善整合包同步与启动器交互
2026-09-16 17:01:22 +08:00

339 lines
9.7 KiB
Rust

//! Authentication flow interface
use chrono::{DateTime, Utc};
use reqwest::StatusCode;
use serde::Serialize;
use std::time::Duration;
use uuid::Uuid;
use crate::State;
pub use crate::state::InstancePlayer;
pub use crate::state::YggdrasilLoginResult;
use crate::state::{
Credentials, MinecraftAccountType, MinecraftLoginFlow, MinecraftProfile,
YggdrasilAccount,
};
pub use crate::state::{MinecraftDeviceLoginFlow, MinecraftDeviceLoginPoll};
use crate::util::fetch::INSECURE_REQWEST_CLIENT;
const STARLIGHT_SESSION_SERVER_URL: &str = "https://skin.starlight.cool/yggdrasil/sessionserver/session/minecraft/hasJoined";
#[tracing::instrument]
pub async fn check_reachable() -> crate::Result<()> {
let resp = INSECURE_REQWEST_CLIENT
.get(STARLIGHT_SESSION_SERVER_URL)
.timeout(Duration::from_secs(5))
.send()
.await?;
if resp.status() == StatusCode::NO_CONTENT {
return Ok(());
}
resp.error_for_status()?;
Ok(())
}
pub async fn set_mojang_auth_use_mirror(
use_mirror: bool,
automatic: bool,
) -> crate::Result<()> {
let state = State::get().await?;
state.set_mojang_auth_use_mirror(use_mirror);
if use_mirror && automatic {
tracing::info!(
"Mojang services are unreachable; routing Mojang service requests through the Fallen proxy"
);
}
Ok(())
}
#[derive(Clone, Debug, Serialize)]
pub struct MojangServiceStatus {
pub service: &'static str,
pub url: &'static str,
pub reachable: bool,
}
const MOJANG_SERVICES: [(&str, &str); 5] = [
("auth", "https://authserver.mojang.com/"),
("account", "https://api.mojang.com/"),
(
"session",
"https://sessionserver.mojang.com/session/minecraft/hasJoined",
),
("services", "https://api.minecraftservices.com/"),
(
"profiles",
"https://api.mojang.com/users/profiles/minecraft/",
),
];
#[tracing::instrument]
pub async fn check_mojang_services() -> Vec<MojangServiceStatus> {
futures::future::join_all(MOJANG_SERVICES.map(
|(service, url)| async move {
let reachable = INSECURE_REQWEST_CLIENT
.get(url)
.timeout(Duration::from_secs(5))
.send()
.await
.is_ok();
MojangServiceStatus {
service,
url,
reachable,
}
},
))
.await
}
#[tracing::instrument]
pub async fn begin_login() -> crate::Result<MinecraftLoginFlow> {
let state = State::get().await?;
crate::state::login_begin(&state.pool).await
}
#[tracing::instrument]
pub async fn begin_browser_login() -> crate::Result<MinecraftLoginFlow> {
let state = State::get().await?;
crate::state::browser_login_begin(&state.pool).await
}
#[tracing::instrument]
pub async fn begin_device_login() -> crate::Result<MinecraftDeviceLoginFlow> {
crate::state::device_login_begin().await
}
#[tracing::instrument]
pub async fn poll_device_login(
device_code: &str,
) -> crate::Result<MinecraftDeviceLoginPoll> {
let state = State::get().await?;
crate::state::device_login_poll(device_code, &state.pool).await
}
#[tracing::instrument]
pub async fn finish_login(
code: &str,
state: &str,
flow: MinecraftLoginFlow,
) -> crate::Result<Credentials> {
let app_state = State::get().await?;
crate::state::login_finish(code, state, flow, &app_state.pool).await
}
#[tracing::instrument]
pub async fn add_offline_user(
username: &str,
uuid: Option<Uuid>,
) -> crate::Result<Credentials> {
let state = State::get().await?;
let credentials = match uuid {
Some(uuid) => Credentials::offline_with_uuid(username, uuid)?,
None => Credentials::offline(username)?,
};
if uuid.is_some() {
let users = Credentials::get_all_without_refresh(&state.pool).await?;
if users.contains_key(&credentials.offline_profile.id) {
return Err(crate::ErrorKind::InputError(
"An account with this UUID already exists".to_string(),
)
.as_error());
}
}
credentials.upsert(&state.pool).await?;
Ok(credentials)
}
#[tracing::instrument(skip(password))]
pub async fn begin_yggdrasil_login(
api_root: &str,
login: &str,
password: &str,
) -> crate::Result<YggdrasilLoginResult> {
let state = State::get().await?;
crate::state::begin_yggdrasil_login(api_root, login, password, &state.pool)
.await
}
#[tracing::instrument]
pub async fn finish_yggdrasil_login(
flow_id: uuid::Uuid,
profile_id: uuid::Uuid,
) -> crate::Result<Credentials> {
let state = State::get().await?;
crate::state::finish_yggdrasil_login(flow_id, profile_id, &state.pool).await
}
pub fn normalize_yggdrasil_api_root(api_root: &str) -> crate::Result<String> {
crate::state::normalize_api_root(api_root)
}
pub async fn get_instance_player(
instance_id: &str,
) -> crate::Result<Option<crate::state::InstancePlayer>> {
let state = State::get().await?;
let context =
crate::state::instances::commands::get_instance_launch_context(
instance_id,
&state.pool,
)
.await?
.ok_or_else(|| {
crate::ErrorKind::InputError("Unknown instance".into()).as_error()
})?;
Ok(context.launch_overrides.player)
}
pub async fn set_instance_player(
instance_id: &str,
player: crate::state::InstancePlayer,
) -> crate::Result<()> {
let state = State::get().await?;
let accounts = Credentials::get_all_without_refresh(&state.pool).await?;
let account = accounts.get(&player.id).ok_or_else(|| {
crate::ErrorKind::InputError("所选玩家未登录,请重新登录该账号".into())
.as_error()
})?;
if account.account_type != player.account_type
|| player.skin_site_user.as_ref().is_some_and(|user| {
account.yggdrasil.as_ref().is_none_or(|ygg| {
ygg.login != *user
|| ygg.api_root != "https://skin.starlight.cool/yggdrasil"
})
})
{
return Err(
crate::ErrorKind::InputError("玩家身份不匹配".into()).as_error()
);
}
drop(account);
crate::state::edit_instance(
instance_id,
crate::state::EditInstance {
launch_overrides: Some(
crate::state::InstanceLaunchOverridesPatch {
player: Some(player),
..Default::default()
},
),
..Default::default()
},
&state.pool,
)
.await?;
Ok(())
}
pub async fn login_skin_site_player(
token: &str,
player_id: uuid::Uuid,
user_id: &str,
) -> crate::Result<Credentials> {
let state = State::get().await?;
crate::state::login_skin_site_player(token, player_id, user_id, &state.pool)
.await
}
#[tracing::instrument]
pub async fn get_default_user(
offline_mode: bool,
) -> crate::Result<Option<uuid::Uuid>> {
let state = State::get().await?;
let user = if offline_mode {
Credentials::get_offline_credential(&state.pool).await?
} else {
Credentials::get_active(&state.pool).await?
};
Ok(user.map(|user| user.offline_profile.id))
}
#[tracing::instrument]
pub async fn set_default_user(user: uuid::Uuid) -> crate::Result<()> {
let state = State::get().await?;
let users = Credentials::get_all_without_refresh(&state.pool).await?;
let (_, mut user) = users.remove(&user).ok_or_else(|| {
crate::ErrorKind::OtherError(format!(
"Tried to get nonexistent user with ID {user}"
))
.as_error()
})?;
user.active = true;
user.upsert(&state.pool).await?;
Ok(())
}
/// Remove a user account from the database
#[tracing::instrument]
pub async fn remove_user(uuid: uuid::Uuid) -> crate::Result<()> {
let state = State::get().await?;
let users = Credentials::get_all_without_refresh(&state.pool).await?;
if let Some((uuid, user)) = users.remove(&uuid) {
Credentials::remove(uuid, &state.pool).await?;
if user.active
&& let Some((_, mut user)) = users.into_iter().next()
{
user.active = true;
user.upsert(&state.pool).await?;
}
}
Ok(())
}
#[derive(Serialize)]
pub struct MinecraftUser {
pub profile: MinecraftProfile,
pub account_type: MinecraftAccountType,
pub access_token: String,
pub refresh_token: String,
pub expires: DateTime<Utc>,
pub active: bool,
pub yggdrasil: Option<YggdrasilAccount>,
}
impl MinecraftUser {
async fn from_credentials(credentials: Credentials) -> Self {
let profile = (*credentials.maybe_online_profile().await).clone();
Self {
profile,
account_type: credentials.account_type,
access_token: credentials.access_token,
refresh_token: credentials.refresh_token,
expires: credentials.expires,
active: credentials.active,
yggdrasil: credentials.yggdrasil,
}
}
}
/// Get a copy of the list of all user credentials with profile data ready for
/// serialization.
#[tracing::instrument]
pub async fn users(offline_mode: bool) -> crate::Result<Vec<MinecraftUser>> {
let state = State::get().await?;
let users = if offline_mode {
Credentials::get_all_without_refresh(&state.pool).await?
} else {
Credentials::get_all(&state.pool).await?
};
let credentials = users
.into_iter()
.map(|x| x.1)
.filter(|credentials| !offline_mode || credentials.is_offline());
let mut hydrated_users = Vec::new();
for credentials in credentials {
hydrated_users.push(MinecraftUser::from_credentials(credentials).await);
}
Ok(hydrated_users)
}