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:
Danielle
2022-07-15 15:39:38 +00:00
committed by GitHub
parent 53948c7a5e
commit b223dc7cba
27 changed files with 1490 additions and 851 deletions

View File

@@ -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);

View 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?)
}

View File

@@ -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()),

View File

@@ -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()
})
}