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
339 lines
9.7 KiB
Rust
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)
|
|
}
|