You've already forked AstralRinth
forked from didirus/AstralRinth
Authentication (#37)
* Initial authentication implementation * Store user info in the database, improve encapsulation in profiles * Add user list, remove unused dependencies, add spantraces * Implement user remove, update UUID crate * Add user set-default * Revert submodule macro usage * Make tracing significantly less verbose
This commit is contained in:
@@ -40,10 +40,11 @@ pub fn get_class_paths(
|
||||
client_path
|
||||
.canonicalize()
|
||||
.map_err(|_| {
|
||||
crate::Error::LauncherError(format!(
|
||||
crate::ErrorKind::LauncherError(format!(
|
||||
"Specified class path {} does not exist",
|
||||
client_path.to_string_lossy()
|
||||
))
|
||||
.as_error()
|
||||
})?
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
@@ -70,10 +71,11 @@ pub fn get_lib_path(libraries_path: &Path, lib: &str) -> crate::Result<String> {
|
||||
path.push(get_path_from_artifact(lib.as_ref())?);
|
||||
|
||||
let path = &path.canonicalize().map_err(|_| {
|
||||
crate::Error::LauncherError(format!(
|
||||
crate::ErrorKind::LauncherError(format!(
|
||||
"Library file at path {} does not exist",
|
||||
path.to_string_lossy()
|
||||
))
|
||||
.as_error()
|
||||
})?;
|
||||
|
||||
Ok(path.to_string_lossy().to_string())
|
||||
@@ -104,10 +106,11 @@ pub fn get_jvm_arguments(
|
||||
"-Djava.library.path={}",
|
||||
&natives_path
|
||||
.canonicalize()
|
||||
.map_err(|_| crate::Error::LauncherError(format!(
|
||||
.map_err(|_| crate::ErrorKind::LauncherError(format!(
|
||||
"Specified natives path {} does not exist",
|
||||
natives_path.to_string_lossy()
|
||||
)))?
|
||||
))
|
||||
.as_error())?
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
));
|
||||
@@ -142,10 +145,11 @@ fn parse_jvm_argument(
|
||||
&natives_path
|
||||
.canonicalize()
|
||||
.map_err(|_| {
|
||||
crate::Error::LauncherError(format!(
|
||||
crate::ErrorKind::LauncherError(format!(
|
||||
"Specified natives path {} does not exist",
|
||||
natives_path.to_string_lossy()
|
||||
))
|
||||
.as_error()
|
||||
})?
|
||||
.to_string_lossy(),
|
||||
)
|
||||
@@ -154,10 +158,11 @@ fn parse_jvm_argument(
|
||||
&libraries_path
|
||||
.canonicalize()
|
||||
.map_err(|_| {
|
||||
crate::Error::LauncherError(format!(
|
||||
crate::ErrorKind::LauncherError(format!(
|
||||
"Specified libraries path {} does not exist",
|
||||
libraries_path.to_string_lossy()
|
||||
))
|
||||
.as_error()
|
||||
})?
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
@@ -239,7 +244,7 @@ fn parse_minecraft_argument(
|
||||
.replace("${auth_access_token}", access_token)
|
||||
.replace("${auth_session}", access_token)
|
||||
.replace("${auth_player_name}", username)
|
||||
.replace("${auth_uuid}", &uuid.to_hyphenated().to_string())
|
||||
.replace("${auth_uuid}", &uuid.hyphenated().to_string())
|
||||
.replace("${user_properties}", "{}")
|
||||
.replace("${user_type}", "mojang")
|
||||
.replace("${version_name}", version)
|
||||
@@ -249,10 +254,11 @@ fn parse_minecraft_argument(
|
||||
&game_directory
|
||||
.canonicalize()
|
||||
.map_err(|_| {
|
||||
crate::Error::LauncherError(format!(
|
||||
crate::ErrorKind::LauncherError(format!(
|
||||
"Specified game directory {} does not exist",
|
||||
game_directory.to_string_lossy()
|
||||
))
|
||||
.as_error()
|
||||
})?
|
||||
.to_string_lossy()
|
||||
.to_owned(),
|
||||
@@ -262,10 +268,11 @@ fn parse_minecraft_argument(
|
||||
&assets_directory
|
||||
.canonicalize()
|
||||
.map_err(|_| {
|
||||
crate::Error::LauncherError(format!(
|
||||
crate::ErrorKind::LauncherError(format!(
|
||||
"Specified assets directory {} does not exist",
|
||||
assets_directory.to_string_lossy()
|
||||
))
|
||||
.as_error()
|
||||
})?
|
||||
.to_string_lossy()
|
||||
.to_owned(),
|
||||
@@ -275,10 +282,11 @@ fn parse_minecraft_argument(
|
||||
&assets_directory
|
||||
.canonicalize()
|
||||
.map_err(|_| {
|
||||
crate::Error::LauncherError(format!(
|
||||
crate::ErrorKind::LauncherError(format!(
|
||||
"Specified assets directory {} does not exist",
|
||||
assets_directory.to_string_lossy()
|
||||
))
|
||||
.as_error()
|
||||
})?
|
||||
.to_string_lossy()
|
||||
.to_owned(),
|
||||
@@ -361,17 +369,19 @@ pub async fn get_processor_main_class(
|
||||
Ok(tokio::task::spawn_blocking(move || {
|
||||
let zipfile = std::fs::File::open(&path)?;
|
||||
let mut archive = zip::ZipArchive::new(zipfile).map_err(|_| {
|
||||
crate::Error::LauncherError(format!(
|
||||
crate::ErrorKind::LauncherError(format!(
|
||||
"Cannot read processor at {}",
|
||||
path
|
||||
))
|
||||
.as_error()
|
||||
})?;
|
||||
|
||||
let file = archive.by_name("META-INF/MANIFEST.MF").map_err(|_| {
|
||||
crate::Error::LauncherError(format!(
|
||||
crate::ErrorKind::LauncherError(format!(
|
||||
"Cannot read processor manifest at {}",
|
||||
path
|
||||
))
|
||||
.as_error()
|
||||
})?;
|
||||
|
||||
let reader = BufReader::new(file);
|
||||
|
||||
@@ -1,8 +1,168 @@
|
||||
//! Authentication flow
|
||||
// TODO: Implement authentication
|
||||
#[derive(Debug)]
|
||||
//! Authentication flow based on Hydra
|
||||
use async_tungstenite as ws;
|
||||
use bincode::{Decode, Encode};
|
||||
use chrono::{prelude::*, Duration};
|
||||
use futures::prelude::*;
|
||||
use once_cell::sync::*;
|
||||
use serde::Deserialize;
|
||||
use url::Url;
|
||||
|
||||
pub const HYDRA_URL: Lazy<Url> =
|
||||
Lazy::new(|| Url::parse("https://hydra.modrinth.com").unwrap());
|
||||
|
||||
// Socket messages
|
||||
#[derive(Deserialize)]
|
||||
struct ErrorJSON {
|
||||
error: String,
|
||||
}
|
||||
|
||||
impl ErrorJSON {
|
||||
pub fn unwrap<'a, T: Deserialize<'a>>(data: &'a [u8]) -> crate::Result<T> {
|
||||
if let Ok(err) = serde_json::from_slice::<Self>(data) {
|
||||
Err(crate::ErrorKind::HydraError(err.error).as_error())
|
||||
} else {
|
||||
Ok(serde_json::from_slice::<T>(data)?)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct LoginCodeJSON {
|
||||
login_code: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct TokenJSON {
|
||||
token: String,
|
||||
refresh_token: String,
|
||||
expires_after: u32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ProfileInfoJSON {
|
||||
id: uuid::Uuid,
|
||||
name: String,
|
||||
}
|
||||
|
||||
// Login information
|
||||
#[derive(Encode, Decode)]
|
||||
pub struct Credentials {
|
||||
#[bincode(with_serde)]
|
||||
pub id: uuid::Uuid,
|
||||
pub username: String,
|
||||
pub access_token: String,
|
||||
pub refresh_token: String,
|
||||
#[bincode(with_serde)]
|
||||
pub expires: DateTime<Utc>,
|
||||
_ctor_scope: std::marker::PhantomData<()>,
|
||||
}
|
||||
|
||||
// Implementation
|
||||
pub struct HydraAuthFlow<S: AsyncRead + AsyncWrite + Unpin> {
|
||||
socket: ws::WebSocketStream<S>,
|
||||
}
|
||||
|
||||
impl HydraAuthFlow<ws::tokio::ConnectStream> {
|
||||
pub async fn new() -> crate::Result<Self> {
|
||||
let sock_url = wrap_ref_builder!(
|
||||
it = HYDRA_URL =>
|
||||
{ it.set_scheme("wss").ok() }
|
||||
);
|
||||
let (socket, _) = ws::tokio::connect_async(sock_url.clone()).await?;
|
||||
Ok(Self { socket })
|
||||
}
|
||||
|
||||
pub async fn prepare_login_url(&mut self) -> crate::Result<Url> {
|
||||
let code_resp = self
|
||||
.socket
|
||||
.try_next()
|
||||
.await?
|
||||
.ok_or(
|
||||
crate::ErrorKind::WSClosedError(String::from(
|
||||
"login socket ID",
|
||||
))
|
||||
.as_error(),
|
||||
)?
|
||||
.into_data();
|
||||
let code = ErrorJSON::unwrap::<LoginCodeJSON>(&code_resp)?;
|
||||
Ok(wrap_ref_builder!(
|
||||
it = HYDRA_URL.join("login")? =>
|
||||
{ it.query_pairs_mut().append_pair("id", &code.login_code); }
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn extract_credentials(&mut self) -> crate::Result<Credentials> {
|
||||
// Minecraft bearer token
|
||||
let token_resp = self
|
||||
.socket
|
||||
.try_next()
|
||||
.await?
|
||||
.ok_or(
|
||||
crate::ErrorKind::WSClosedError(String::from(
|
||||
"login socket ID",
|
||||
))
|
||||
.as_error(),
|
||||
)?
|
||||
.into_data();
|
||||
let token = ErrorJSON::unwrap::<TokenJSON>(&token_resp)?;
|
||||
let expires =
|
||||
Utc::now() + Duration::seconds(token.expires_after.into());
|
||||
|
||||
// Get account credentials
|
||||
let info = fetch_info(&token.token).await?;
|
||||
|
||||
// Return structure from response
|
||||
Ok(Credentials {
|
||||
username: info.name,
|
||||
id: info.id,
|
||||
refresh_token: token.refresh_token,
|
||||
access_token: token.token,
|
||||
expires,
|
||||
_ctor_scope: std::marker::PhantomData,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn refresh_credentials(
|
||||
credentials: &mut Credentials,
|
||||
) -> crate::Result<()> {
|
||||
let resp = crate::config::REQWEST_CLIENT
|
||||
.post(HYDRA_URL.join("/refresh")?)
|
||||
.json(
|
||||
&serde_json::json!({ "refresh_token": credentials.refresh_token }),
|
||||
)
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.json::<TokenJSON>()
|
||||
.await?;
|
||||
|
||||
credentials.access_token = resp.token;
|
||||
credentials.refresh_token = resp.refresh_token;
|
||||
credentials.expires =
|
||||
Utc::now() + Duration::seconds(resp.expires_after.into());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn refresh_username(
|
||||
credentials: &mut Credentials,
|
||||
) -> crate::Result<()> {
|
||||
let info = fetch_info(&credentials.access_token).await?;
|
||||
credentials.username = info.name;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Helpers
|
||||
async fn fetch_info(token: &str) -> crate::Result<ProfileInfoJSON> {
|
||||
let url =
|
||||
Url::parse("https://api.minecraftservices.com/minecraft/profile")?;
|
||||
Ok(crate::config::REQWEST_CLIENT
|
||||
.get(url)
|
||||
.header(reqwest::header::AUTHORIZATION, format!("Bearer {token}"))
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.json::<ProfileInfoJSON>()
|
||||
.await?)
|
||||
}
|
||||
|
||||
@@ -13,9 +13,9 @@ use daedalus::{
|
||||
modded::LoaderVersion,
|
||||
};
|
||||
use futures::prelude::*;
|
||||
use std::sync::Arc;
|
||||
use tokio::{fs, sync::OnceCell};
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub async fn download_minecraft(
|
||||
st: &State,
|
||||
version: &GameVersionInfo,
|
||||
@@ -33,6 +33,7 @@ pub async fn download_minecraft(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all, fields(version = version.id.as_str(), loader = ?loader))]
|
||||
pub async fn download_version_info(
|
||||
st: &State,
|
||||
version: &GameVersion,
|
||||
@@ -69,6 +70,7 @@ pub async fn download_version_info(
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub async fn download_client(
|
||||
st: &State,
|
||||
version_info: &GameVersionInfo,
|
||||
@@ -78,9 +80,12 @@ pub async fn download_client(
|
||||
let client_download = version_info
|
||||
.downloads
|
||||
.get(&d::minecraft::DownloadType::Client)
|
||||
.ok_or(crate::Error::LauncherError(format!(
|
||||
"No client downloads exist for version {version}"
|
||||
)))?;
|
||||
.ok_or(
|
||||
crate::ErrorKind::LauncherError(format!(
|
||||
"No client downloads exist for version {version}"
|
||||
))
|
||||
.as_error(),
|
||||
)?;
|
||||
let path = st
|
||||
.directories
|
||||
.version_dir(version)
|
||||
@@ -99,6 +104,7 @@ pub async fn download_client(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub async fn download_assets_index(
|
||||
st: &State,
|
||||
version: &GameVersionInfo,
|
||||
@@ -126,6 +132,7 @@ pub async fn download_assets_index(
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(st, index))]
|
||||
pub async fn download_assets(
|
||||
st: &State,
|
||||
with_legacy: bool,
|
||||
@@ -180,16 +187,13 @@ pub async fn download_assets(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(st, libraries))]
|
||||
pub async fn download_libraries(
|
||||
st: &State,
|
||||
libraries: &[Library],
|
||||
version: &str,
|
||||
) -> crate::Result<()> {
|
||||
log::debug!("Loading libraries");
|
||||
let (libraries_dir, natives_dir) = (
|
||||
Arc::new(st.directories.libraries_dir()),
|
||||
Arc::new(st.directories.version_natives_dir(version)),
|
||||
);
|
||||
|
||||
tokio::try_join! {
|
||||
fs::create_dir_all(st.directories.libraries_dir()),
|
||||
|
||||
@@ -6,11 +6,11 @@ use tokio::process::{Child, Command};
|
||||
|
||||
mod args;
|
||||
|
||||
mod auth;
|
||||
pub use auth::Credentials;
|
||||
pub mod auth;
|
||||
|
||||
mod download;
|
||||
|
||||
#[tracing::instrument]
|
||||
pub fn parse_rule(rule: &d::minecraft::Rule) -> bool {
|
||||
use d::minecraft::{Rule, RuleAction};
|
||||
|
||||
@@ -44,6 +44,7 @@ macro_rules! processor_rules {
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all, fields(path = ?instance_path))]
|
||||
pub async fn launch_minecraft(
|
||||
game_version: &str,
|
||||
loader_version: &Option<d::modded::LoaderVersion>,
|
||||
@@ -64,7 +65,7 @@ pub async fn launch_minecraft(
|
||||
.versions
|
||||
.iter()
|
||||
.find(|it| it.id == game_version)
|
||||
.ok_or(crate::Error::LauncherError(format!(
|
||||
.ok_or(crate::ErrorKind::LauncherError(format!(
|
||||
"Invalid game version: {game_version}"
|
||||
)))?;
|
||||
|
||||
@@ -115,8 +116,9 @@ pub async fn launch_minecraft(
|
||||
}
|
||||
}
|
||||
|
||||
let mut cp = processor.classpath.clone();
|
||||
cp.push(processor.jar.clone());
|
||||
let cp = wrap_ref_builder!(cp = processor.classpath.clone() => {
|
||||
cp.push(processor.jar.clone())
|
||||
});
|
||||
|
||||
let child = Command::new("java")
|
||||
.arg("-cp")
|
||||
@@ -131,7 +133,7 @@ pub async fn launch_minecraft(
|
||||
)?)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
crate::Error::LauncherError(format!(
|
||||
crate::ErrorKind::LauncherError(format!(
|
||||
"Could not find processor main class for {}",
|
||||
processor.jar
|
||||
))
|
||||
@@ -145,16 +147,17 @@ pub async fn launch_minecraft(
|
||||
.output()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
crate::Error::LauncherError(format!(
|
||||
crate::ErrorKind::LauncherError(format!(
|
||||
"Error running processor: {err}",
|
||||
))
|
||||
})?;
|
||||
|
||||
if !child.status.success() {
|
||||
return Err(crate::Error::LauncherError(format!(
|
||||
return Err(crate::ErrorKind::LauncherError(format!(
|
||||
"Processor error: {}",
|
||||
String::from_utf8_lossy(&child.stderr)
|
||||
)));
|
||||
))
|
||||
.as_error());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -163,9 +166,7 @@ pub async fn launch_minecraft(
|
||||
let args = version_info.arguments.clone().unwrap_or_default();
|
||||
let mut command = match wrapper {
|
||||
Some(hook) => {
|
||||
let mut cmd = Command::new(hook);
|
||||
cmd.arg(java_install);
|
||||
cmd
|
||||
wrap_ref_builder!(it = Command::new(hook) => {it.arg(java_install)})
|
||||
}
|
||||
None => Command::new(String::from(java_install.to_string_lossy())),
|
||||
};
|
||||
@@ -203,10 +204,11 @@ pub async fn launch_minecraft(
|
||||
.stderr(Stdio::inherit());
|
||||
|
||||
command.spawn().map_err(|err| {
|
||||
crate::Error::LauncherError(format!(
|
||||
crate::ErrorKind::LauncherError(format!(
|
||||
"Error running Minecraft (minecraft-{} @ {}): {err}",
|
||||
&version.id,
|
||||
instance_path.display()
|
||||
))
|
||||
.as_error()
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user