summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--ozone-cli/src/cli.rs24
-rw-r--r--ozone-cli/src/main.rs192
-rw-r--r--ozone/src/auth/types.rs81
-rw-r--r--ozone/src/launcher.rs3
-rw-r--r--ozone/src/launcher/constants.rs3
5 files changed, 284 insertions, 19 deletions
diff --git a/ozone-cli/src/cli.rs b/ozone-cli/src/cli.rs
index 6ba23d3..8ed2f33 100644
--- a/ozone-cli/src/cli.rs
+++ b/ozone-cli/src/cli.rs
@@ -176,11 +176,33 @@ pub struct ProfileSelectArgs {
pub gamertag: Option<String>
}
+#[derive(Args, Debug)]
+pub struct ProfileSignInArgs {
+ /// Elect to use an alternate client id to authenticate to Minecraft.
+ ///
+ /// Avoid using this if possible,
+ /// since you will not be able to easily revoke oauth access to the launcher.
+ ///
+ /// Situations when you might need to use this are if your Microsoft account's birthday
+ /// is set to a date fewer than 18 years in the past while not in a family.
+ #[arg(long)]
+ pub use_alt_client_id: bool,
+
+ #[arg(long)]
+ pub no_select: bool
+}
+
#[derive(Subcommand, Debug)]
pub enum AccountCommand {
Select(ProfileSelectArgs),
Forget,
- SignIn
+
+ #[command(name = "signin")]
+ SignIn(ProfileSignInArgs),
+
+ List,
+ Info,
+ Refresh
}
#[derive(Args, Debug)]
diff --git a/ozone-cli/src/main.rs b/ozone-cli/src/main.rs
index 62f7155..f033a19 100644
--- a/ozone-cli/src/main.rs
+++ b/ozone-cli/src/main.rs
@@ -5,22 +5,23 @@ use std::path::Path;
use std::process::ExitCode;
use log::{error, info, trace, LevelFilter};
use clap::Parser;
-use ozone::launcher::{Instance, JavaRuntimeSetting, Launcher, Settings};
+use ozone::launcher::{Instance, JavaRuntimeSetting, Launcher, Settings, ALT_CLIENT_ID, MAIN_CLIENT_ID};
use ozone::launcher::version::{VersionList, VersionResult};
use uuid::Uuid;
-use ozone::auth::{Account, AuthenticationDatabase};
+use ozone::auth::{Account, AccountStorage, MsaAccount};
+use ozone::auth::error::AuthErrorKind;
use crate::cli::{AccountCommand, Cli, InstanceCommand, ProfileSelectArgs, RootCommand};
-fn find_account<'a>(auth: &'a AuthenticationDatabase, input: &ProfileSelectArgs) -> Result<Vec<&'a Account>, ()> {
+fn find_account<'a>(auth: &'a AccountStorage, input: &ProfileSelectArgs) -> Result<Vec<(&'a String, &'a Account)>, ()> {
if let Some(uuid) = input.uuid {
- Ok(auth.users.iter().filter(|a| match *a {
+ Ok(auth.iter_accounts().filter(|(_, a)| match *a {
Account::Dummy(p) => p.id == uuid,
Account::MSA(account) => account.player_profile.as_ref().is_some_and(|p| p.id == uuid)
}).collect())
} else if let Some(ref name) = input.name {
let name = name.to_ascii_lowercase();
- Ok(auth.users.iter().filter(|a| match *a {
+ Ok(auth.iter_accounts().filter(|(_, a)| match *a {
Account::Dummy(profile) => profile.name.to_ascii_lowercase().starts_with(&name),
Account::MSA(account) =>
account.player_profile.as_ref().is_some_and(|p| p.name.to_ascii_lowercase().starts_with(&name))
@@ -28,12 +29,12 @@ fn find_account<'a>(auth: &'a AuthenticationDatabase, input: &ProfileSelectArgs)
} else if let Some(ref gt) = input.gamertag {
let gt = gt.to_ascii_lowercase();
- Ok(auth.users.iter().filter(|a| match *a {
+ Ok(auth.iter_accounts().filter(|(_, a)| match *a {
Account::MSA(account) => account.gamertag.as_ref().is_some_and(|g| g.to_ascii_lowercase().starts_with(&gt)),
_ => false
}).collect())
} else if let Some(ref xuid) = input.xuid {
- Ok(auth.users.iter().filter(|a| match *a {
+ Ok(auth.iter_accounts().filter(|(_, a)| match *a {
Account::MSA(account) => account.xuid.as_ref().is_some_and(|x| x == xuid),
_ => false
}).collect())
@@ -66,6 +67,45 @@ fn display_instance(instance: &Instance, id: Uuid, home: impl AsRef<Path>, selec
}
}
+fn display_account(account: &Account, selected: bool, verbose: bool) {
+ let selected = if selected { " (selected)" } else { "" };
+
+ match *account {
+ Account::Dummy(ref profile) => {
+ println!("Dummy account:{selected}");
+ println!(" Username: {}", profile.name);
+ println!(" UUID: {}", profile.id);
+ if verbose {
+ println!(" Properties: <{} propert{}>", profile.properties.len(), if profile.properties.len() == 1 { "y" } else { "ies" });
+ }
+ },
+ Account::MSA(ref msa_acct) => {
+ println!("Microsoft account:{selected}");
+
+ if let Some(ref profile) = msa_acct.player_profile {
+ println!(" Username: {}", profile.name);
+ println!(" UUID: {}", profile.id);
+
+ if verbose {
+ println!(" Properties: <{} propert{}>", profile.properties.len(), if profile.properties.len() == 1 { "y" } else { "ies" });
+ }
+ } else {
+ println!(" Username: <no profile>");
+ println!(" UUID: <no profile>");
+
+ if verbose {
+ println!(" Properties: <no profile>");
+ }
+ }
+
+ println!(" Xbox Gamertag: {}", msa_acct.gamertag.as_deref().unwrap_or("<unknown>"));
+ if verbose {
+ println!(" XUID: {}", msa_acct.xuid.as_deref().unwrap_or("<unknown>"));
+ }
+ }
+ }
+}
+
async fn main_inner(cli: Cli) -> Result<ExitCode, Box<dyn Error>> {
let Some(home) = cli.home.or_else(Launcher::sensible_home) else {
error!("Could not choose a launcher home directory. Please choose one with `--home'.");
@@ -81,7 +121,7 @@ async fn main_inner(cli: Cli) -> Result<ExitCode, Box<dyn Error>> {
let mut first = true;
if settings.instances.is_empty() {
- eprintln!("There are no instances. Create one with `profile create <name>'.");
+ eprintln!("There are no instances. Create one with `instance create <name>'.");
return Ok(ExitCode::SUCCESS);
}
@@ -220,17 +260,149 @@ async fn main_inner(cli: Cli) -> Result<ExitCode, Box<dyn Error>> {
}
},
RootCommand::Account(account_args) => {
- // TODO: load auth db and do stuff with it
+ let accounts_path = home.join("ozone_accounts.json");
+ let mut accounts = AccountStorage::load(&accounts_path).await?;
match &account_args.command {
AccountCommand::Select(args) => {
+ let Ok(results) = find_account(&accounts, args) else {
+ return Ok(ExitCode::FAILURE);
+ };
+
+ if results.is_empty() {
+ eprintln!("No account was found.");
+ return Ok(ExitCode::FAILURE);
+ }
+
+ if results.len() > 1 {
+ eprintln!("Ambiguous argument. Found {} accounts:", results.len());
+ for (_, account) in results {
+ eprintln!("- {account}");
+ }
+ return Ok(ExitCode::FAILURE);
+ }
+
+ let (key, account) = results.into_iter().next().unwrap();
+ println!("Selected account: {account}");
+ let key = key.clone();
+ accounts.set_selected_account(key);
+ accounts.save(&accounts_path).await?;
},
AccountCommand::Forget => {
+ if accounts.pop_selected_account().is_none() {
+ eprintln!("No account selected.");
+ return Ok(ExitCode::FAILURE);
+ }
+
+ accounts.save(&accounts_path).await?;
+ },
+ AccountCommand::SignIn(args) => {
+ let (client_id, azure) = if args.use_alt_client_id {
+ (ALT_CLIENT_ID, false)
+ } else {
+ (MAIN_CLIENT_ID, true)
+ };
+
+ let client = MsaAccount::create_client();
+
+ let mut acct = MsaAccount::with_client_id(client_id, azure);
+ acct.xbl_login_device(&client, |d| async move {
+ if let Some(uri_complete) = d.verification_uri_complete() {
+ println!("In a browser, please navigate to the following URL:");
+ println!("{}", uri_complete.secret());
+ } else {
+ println!("In a browser, please navigate to the following URL: {}", d.verification_uri());
+ println!("Use the following device code: {}", d.user_code().secret())
+ }
+ }).await?;
+
+ println!("Authentication success! Logging in...");
+
+ match acct.log_in_silent(&client).await {
+ Ok(_) => (),
+ Err(e) => match e.kind() {
+ AuthErrorKind::NotOnXbox => {
+ eprintln!("This Microsoft account is not on Xbox. Please make sure you are using the correct Microsoft account.");
+ return Ok(ExitCode::FAILURE);
+ },
+ AuthErrorKind::TooYoung => {
+ eprintln!("Currently, Microsoft accounts held by minors (under 18 years old) cannot sign into third party applications (such as olauncher) unless they are in a family.");
+ eprintln!("If you do not wish to configure a family, try running this command with the `--use-alt-client-id' flag.");
+ return Ok(ExitCode::FAILURE);
+ },
+ AuthErrorKind::NotEntitled => {
+ eprintln!("Warning: This Microsoft account does not seem to own the game.");
+ eprintln!("This account will only be able to play the demo version of the game.");
+ },
+ AuthErrorKind::NoProfile => {
+ eprintln!("Warning: It appears that you own the game but have not yet created a profile.");
+ eprintln!("Visit https://minecraft.net to choose a name. Until then, you will only be able to play the demo version of the game.");
+ },
+ _ => {
+ eprintln!("An unknown error occurred while signing into the account:");
+ eprintln!("{e}");
+ return Ok(ExitCode::FAILURE);
+ }
+ }
+ }
+
+ let key = accounts.add_account(acct.into()).expect("authentication succeeded but xuid missing????");
+ if !args.no_select {
+ accounts.set_selected_account(key);
+ }
+
+ accounts.save(&accounts_path).await?;
+
+ println!("Success! Account added.");
+ },
+ AccountCommand::List => {
+ let iter = accounts.iter_accounts();
+ if iter.len() == 0 {
+ eprintln!("There are no accounts.");
+ return Ok(ExitCode::FAILURE);
+ }
+
+ let sel_id = accounts.get_selected_account().map(|(id, _)| id);
+ let mut first = true;
+ for (id, account) in iter {
+ if !first {
+ println!();
+ }
+
+ first = false;
+ display_account(account, sel_id.is_some_and(|s| s == id), false);
+ }
},
- AccountCommand::SignIn => {
+ AccountCommand::Info => {
+ let Some((_, account)) = accounts.get_selected_account() else {
+ eprintln!("No account selected.");
+ return Ok(ExitCode::FAILURE);
+ };
+ display_account(account, false, true);
+ },
+ AccountCommand::Refresh => {
+ let Some(account) = accounts.get_selected_account_mut() else {
+ eprintln!("No account selected.");
+ return Ok(ExitCode::FAILURE);
+ };
+
+ let client = MsaAccount::create_client();
+
+ match account {
+ Account::MSA(msa_acct) => {
+ msa_acct.log_in_silent(&client).await?;
+ println!("Successfully refreshed account: {}", account);
+ },
+ _ => {
+ eprintln!("Cannot refresh non-MSA account.");
+ return Ok(ExitCode::FAILURE);
+ }
+ }
+
+ accounts.save(&accounts_path).await?;
}
}
}
diff --git a/ozone/src/auth/types.rs b/ozone/src/auth/types.rs
index 134c45a..a2ac7da 100644
--- a/ozone/src/auth/types.rs
+++ b/ozone/src/auth/types.rs
@@ -1,11 +1,16 @@
pub mod property_map;
+
+use std::collections::HashMap;
pub use property_map::PropertyMap;
use std::fmt::{Debug, Display, Formatter};
+use std::io::ErrorKind;
+use std::path::Path;
use chrono::{DateTime, Utc};
use multimap::MultiMap;
use oauth2::RefreshToken;
use serde::{Deserialize, Serialize};
+use tokio::io;
use uuid::Uuid;
#[derive(Debug, Serialize, Deserialize)]
@@ -18,7 +23,7 @@ pub struct Property {
}
#[derive(Debug, Serialize, Deserialize)]
-#[non_exhaustive]
+#[non_exhaustive] // instantiating this struct requires some kind of validation
pub struct PlayerProfile {
#[serde(with = "uuid::serde::simple")]
pub id: Uuid,
@@ -157,9 +162,73 @@ pub enum Account {
MSA(Box<MsaAccount>)
}
-#[derive(Debug, Serialize, Deserialize)]
-pub struct AuthenticationDatabase {
- pub users: Vec<Account>
+impl From<MsaAccount> for Account {
+ fn from(value: MsaAccount) -> Self {
+ Self::MSA(value.into())
+ }
+}
+
+impl Account {
+ fn get_key(&self) -> Option<String> {
+ match self {
+ Account::Dummy(profile) => Some(format!("dummy:{}", profile.id)),
+ Account::MSA(account) => account.xuid.as_ref().map(|x| format!("msa:xuid:{x}"))
+ }
+ }
+}
+
+#[derive(Debug, Default, Serialize, Deserialize)]
+pub struct AccountStorage {
+ #[serde(default)]
+ accounts: HashMap<String, Account>,
+
+ #[serde(skip_serializing_if = "Option::is_none")]
+ selected_account: Option<String>
+}
+
+impl AccountStorage {
+ pub async fn load(path: impl AsRef<Path>) -> Result<AccountStorage, io::Error> {
+ let contents = match tokio::fs::read_to_string(&path).await {
+ Ok(c) => c,
+ Err(e) if e.kind() == ErrorKind::NotFound => return Ok(Default::default()),
+ Err(e) => return Err(e)
+ };
+
+ serde_json::from_str(&contents).map_err(|e| io::Error::new(ErrorKind::InvalidData, e))
+ }
+
+ pub async fn save(&self, path: impl AsRef<Path>) -> Result<(), io::Error> {
+ let contents = serde_json::to_string_pretty(self) // should be infallible but whatever
+ .map_err(|e| io::Error::new(ErrorKind::InvalidData, e))?;
+ tokio::fs::write(&path, &contents).await
+ }
+
+ pub fn add_account(&mut self, account: Account) -> Result<String, ()> {
+ let key = account.get_key().ok_or(())?;
+ self.accounts.insert(key.clone(), account);
+
+ Ok(key)
+ }
+
+ pub fn get_selected_account(&self) -> Option<(&String, &Account)> {
+ self.selected_account.as_ref().and_then(|a| self.selected_account.as_ref().zip(self.accounts.get(a)))
+ }
+
+ pub fn get_selected_account_mut(&mut self) -> Option<&mut Account> {
+ self.selected_account.as_ref().and_then(|a| self.accounts.get_mut(a))
+ }
+
+ pub fn pop_selected_account(&mut self) -> Option<Account> {
+ self.selected_account.as_ref().and_then(|a| self.accounts.remove(a))
+ }
+
+ pub fn set_selected_account(&mut self, key: impl Into<String>) {
+ self.selected_account = Some(key.into());
+ }
+
+ pub fn iter_accounts(&self) -> impl ExactSizeIterator<Item = (&String, &Account)> {
+ self.accounts.iter()
+ }
}
impl Display for Account {
@@ -167,8 +236,8 @@ impl Display for Account {
match self {
Account::Dummy(profile) => write!(f, "[dummy] {}", profile.name),
Account::MSA(account) => write!(f, "[msa] {} ({})",
- account.gamertag.as_deref().unwrap_or("???"),
- account.player_profile.as_ref().map(|p| p.name.as_str()).unwrap_or("???"))
+ account.player_profile.as_ref().map(|p| p.name.as_str()).unwrap_or("???"),
+ account.gamertag.as_deref().unwrap_or("???"))
}
}
}
diff --git a/ozone/src/launcher.rs b/ozone/src/launcher.rs
index 8c5db3f..9add08c 100644
--- a/ozone/src/launcher.rs
+++ b/ozone/src/launcher.rs
@@ -24,7 +24,6 @@ use std::time::{Instant, SystemTime, UNIX_EPOCH};
use futures::{StreamExt, TryStreamExt};
use indexmap::IndexMap;
use log::{debug, info, trace, warn};
-use reqwest::Client;
use sysinfo::System;
use tokio::{fs, io};
use tokio_stream::wrappers::ReadDirStream;
@@ -38,12 +37,12 @@ use assets::{AssetError, AssetRepository};
use ozone_helpers::ozone_arch_bits;
use crate::util::{self, AsJavaPath};
+pub use constants::{MAIN_CLIENT_ID, ALT_CLIENT_ID};
pub use settings::*;
pub use runner::run_the_game;
pub use crate::util::{EnsureFileError, FileVerifyError, IntegrityError};
use crate::assets::AssetIndex;
use runner::ArgumentType;
-use strsub::SubFunc;
use crate::launcher::download::FileDownload;
use crate::launcher::jre::{JavaRuntimeError, JavaRuntimeRepository};
use crate::launcher::version::VersionError;
diff --git a/ozone/src/launcher/constants.rs b/ozone/src/launcher/constants.rs
index 78e53f5..189ec82 100644
--- a/ozone/src/launcher/constants.rs
+++ b/ozone/src/launcher/constants.rs
@@ -9,6 +9,9 @@ pub const NATIVES_PREFIX: &str = "natives-";
pub const DEF_INSTANCE_NAME: &str = "Default";
+pub const MAIN_CLIENT_ID: &str = "60b6cc54-fc07-4bab-bca9-cbe9aa713c80"; // the standard olauncher client ID
+pub const ALT_CLIENT_ID: &str = "00000000402b5328"; // regular Minecraft launcher client id
+
// https://github.com/unmojang/FjordLauncher/pull/14/files
// https://login.live.com/oauth20_authorize.srf?client_id=00000000402b5328&redirect_uri=ms-xal-00000000402b5328://auth&response_type=token&display=touch&scope=service::user.auth.xboxlive.com::MBI_SSL%20offline_access&prompt=select_account