error handling part 2
This commit is contained in:
parent
a8e6c2449b
commit
b2f459478a
8 changed files with 97 additions and 49 deletions
|
|
@ -1,12 +1,39 @@
|
|||
use std::{error::Error, io::{Read, Write}, net::{SocketAddr, TcpListener, TcpStream}};
|
||||
use std::{io::{Read, Write}, net::{SocketAddr, TcpListener, TcpStream}};
|
||||
|
||||
use serde_json::json;
|
||||
use tauri::{AppHandle, Runtime};
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{constants::CLIENT_ID, utils::http::create_client, PolyResult};
|
||||
use crate::{constants::CLIENT_ID, utils::http::create_client, PolyError, PolyResult};
|
||||
|
||||
use super::{Account, AuthenticationError, AuthenticationMethod};
|
||||
|
||||
// TODO: store auth keys with https://beta.tauri.app/features/stronghold/
|
||||
// TODO: store auth key public in prisma for easy access
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum MicrosoftAuthError {
|
||||
#[error("failed to get Minecraft access token")]
|
||||
MSAccessToken,
|
||||
#[error("failed to get XSTS token")]
|
||||
XSTSToken,
|
||||
#[error("failed to get XBL token")]
|
||||
XBLToken,
|
||||
#[error("failed to get XBL user hash")]
|
||||
XBLUserHash,
|
||||
#[error("this account doesn't have an xbox account, please sign in through minecraft.net and try again")]
|
||||
NoXBoxAccount,
|
||||
#[error("this account is from a country where xbox live is not available")]
|
||||
XBoxLiveBlocked,
|
||||
#[error("this account needs adult verification on the xbox page")]
|
||||
AdultVerification,
|
||||
#[error("this account is under 18, please add this account to a family by an adult and try again")]
|
||||
ChildAccount,
|
||||
#[error("unknown xbox live error: {0}")]
|
||||
UnknownError(u64)
|
||||
}
|
||||
|
||||
use super::{Account, AuthenticationMethod};
|
||||
|
||||
pub(super) struct MicrosoftAuthenticationMethod;
|
||||
impl AuthenticationMethod for MicrosoftAuthenticationMethod {
|
||||
|
|
@ -35,8 +62,8 @@ impl AuthenticationMethod for MicrosoftAuthenticationMethod {
|
|||
}
|
||||
}
|
||||
|
||||
async fn auth_minecraft(xsts_token: String, user_hash: String) -> Result<String, Box<dyn Error>> {
|
||||
let response = create_client()
|
||||
async fn auth_minecraft(xsts_token: String, user_hash: String) -> PolyResult<String> {
|
||||
let response = create_client()?
|
||||
.post("https://api.minecraftservices.com/authentication/login_with_xbox")
|
||||
.json(&json!({
|
||||
"identityToken": format!("XBL3.0 x={};{}", user_hash, xsts_token)
|
||||
|
|
@ -47,14 +74,14 @@ async fn auth_minecraft(xsts_token: String, user_hash: String) -> Result<String,
|
|||
let response = response.json::<serde_json::Value>().await?;
|
||||
let access_token = match response.get("access_token") {
|
||||
Some(token) => token.as_str().unwrap(),
|
||||
None => return Err("Failed to get Minecraft access token".into())
|
||||
None => return Err(PolyError::AuthError(AuthenticationError::MicrosoftError(MicrosoftAuthError::MSAccessToken)))
|
||||
};
|
||||
|
||||
Ok(access_token.to_string())
|
||||
}
|
||||
|
||||
async fn auth_xsts(token: String) -> Result<String, Box<dyn Error>> {
|
||||
let response = create_client()
|
||||
async fn auth_xsts(token: String) -> PolyResult<String> {
|
||||
let response = create_client()?
|
||||
.post("https://xsts.auth.xboxlive.com/xsts/authorize")
|
||||
.json(&json!({
|
||||
"Properties": {
|
||||
|
|
@ -70,28 +97,28 @@ async fn auth_xsts(token: String) -> Result<String, Box<dyn Error>> {
|
|||
.await?;
|
||||
|
||||
if response.status().as_u16() == 401 {
|
||||
// 2148916233: The account doesn't have an Xbox account. Once they sign up for one (or login through minecraft.net to create one) then they can proceed with the login. This shouldn't happen with accounts that have purchased Minecraft with a Microsoft account, as they would've already gone through that Xbox signup process.
|
||||
// 2148916235: The account is from a country where Xbox Live is not available/banned
|
||||
// 2148916236: The account needs adult verification on Xbox page. (South Korea)
|
||||
// 2148916237: The account needs adult verification on Xbox page. (South Korea)
|
||||
// 2148916238: The account is a child (under 18) and cannot proceed unless the account is added to a Family by an adult. This only seems to occur when using a custom Microsoft Azure application. When using the Minecraft launchers client id, this doesn't trigger.
|
||||
// TODO: Handle these errors
|
||||
|
||||
let error_code = response.json::<serde_json::Value>().await?["XErr"].as_u64().unwrap();
|
||||
return Err(format!("Failed to authenticate with Xbox Live. Code: {}", error_code).into());
|
||||
|
||||
match error_code {
|
||||
2148916233 => return Err(PolyError::AuthError(AuthenticationError::MicrosoftError(MicrosoftAuthError::NoXBoxAccount))),
|
||||
2148916235 => return Err(PolyError::AuthError(AuthenticationError::MicrosoftError(MicrosoftAuthError::XBoxLiveBlocked))),
|
||||
2148916236 | 2148916237 => return Err(PolyError::AuthError(AuthenticationError::MicrosoftError(MicrosoftAuthError::AdultVerification))),
|
||||
2148916238 => return Err(PolyError::AuthError(AuthenticationError::MicrosoftError(MicrosoftAuthError::ChildAccount))),
|
||||
_ => return Err(PolyError::AuthError(AuthenticationError::MicrosoftError(MicrosoftAuthError::UnknownError(error_code)))),
|
||||
}
|
||||
}
|
||||
|
||||
let response = response.json::<serde_json::Value>().await?;
|
||||
let xsts_token = match response.get("Token") {
|
||||
Some(token) => token.as_str().unwrap(),
|
||||
None => return Err("Failed to get XSTS token".into())
|
||||
None => return Err(PolyError::AuthError(AuthenticationError::MicrosoftError(MicrosoftAuthError::XSTSToken)))
|
||||
};
|
||||
|
||||
Ok(xsts_token.to_string())
|
||||
}
|
||||
|
||||
async fn auth_xbl(code: String) -> Result<(String, String), Box<dyn Error>> {
|
||||
let response = create_client()
|
||||
async fn auth_xbl(code: String) -> PolyResult<(String, String)> {
|
||||
let response = create_client()?
|
||||
.post("https://user.auth.xboxlive.com/user/authenticate")
|
||||
.json(&json!({
|
||||
"Properties": {
|
||||
|
|
@ -108,19 +135,19 @@ async fn auth_xbl(code: String) -> Result<(String, String), Box<dyn Error>> {
|
|||
let response = response.json::<serde_json::Value>().await?;
|
||||
let xbl_token = match response.get("Token") {
|
||||
Some(token) => token.as_str().unwrap(),
|
||||
None => return Err("Failed to get XBL token".into())
|
||||
None => return Err(PolyError::AuthError(AuthenticationError::MicrosoftError(MicrosoftAuthError::XBLToken)))
|
||||
};
|
||||
|
||||
let user_hash = match response.get("DisplayClaims").and_then(|claims| claims.get("xui").and_then(|xui| xui.get(0).and_then(|xui| xui.get("uhs")))) {
|
||||
Some(hash) => hash.as_str().unwrap(),
|
||||
None => return Err("Failed to get user hash".into())
|
||||
None => return Err(PolyError::AuthError(AuthenticationError::MicrosoftError(MicrosoftAuthError::XBLUserHash)))
|
||||
};
|
||||
|
||||
Ok((xbl_token.to_string(), user_hash.to_string()))
|
||||
}
|
||||
|
||||
async fn msa_code_to_token(code: String) -> Result<String, Box<dyn Error>> {
|
||||
let token = create_client()
|
||||
async fn msa_code_to_token(code: String) -> PolyResult<String> {
|
||||
let token = create_client()?
|
||||
.post("https://login.live.com/oauth20_token.srf")
|
||||
.form(&[
|
||||
("client_id", CLIENT_ID),
|
||||
|
|
@ -137,7 +164,7 @@ async fn msa_code_to_token(code: String) -> Result<String, Box<dyn Error>> {
|
|||
}
|
||||
|
||||
const PORT: u16 = 13523;
|
||||
async fn msa_code<R: Runtime>(handle: &AppHandle<R>) -> Result<String, Box<dyn Error>> {
|
||||
async fn msa_code<R: Runtime>(handle: &AppHandle<R>) -> PolyResult<String> {
|
||||
let url = format!(
|
||||
"https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize?client_id={}&response_type=code&redirect_uri=http://localhost:{}/&response_mode=query&scope=XboxLive.signin%20offline_access&prompt=consent",
|
||||
CLIENT_ID,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
use anyhow::anyhow;
|
||||
use thiserror::Error;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::{plugin::TauriPlugin, AppHandle, Manager, Runtime};
|
||||
use tauri_plugin_http::reqwest::Client;
|
||||
|
|
@ -8,6 +10,12 @@ use self::microsoft_auth::MicrosoftAuthenticationMethod;
|
|||
|
||||
mod microsoft_auth;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum AuthenticationError {
|
||||
#[error(transparent)]
|
||||
MicrosoftError(#[from] microsoft_auth::MicrosoftAuthError)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Account {
|
||||
pub username: String,
|
||||
|
|
@ -39,7 +47,7 @@ pub trait AuthenticationMethod {
|
|||
|
||||
let response = response.json::<serde_json::Value>().await?;
|
||||
if let Some(error) = response.get("error") {
|
||||
return Err(PolyError(error.to_string()));
|
||||
return Err(PolyError::AnyhowError(anyhow!(error.to_string())));
|
||||
}
|
||||
|
||||
let account = serde_json::from_value::<Account>(response)?;
|
||||
|
|
|
|||
|
|
@ -173,7 +173,7 @@ async fn get_java_versions(java_version: &JavaVersion, os: &str, archive_type: &
|
|||
archive_type
|
||||
);
|
||||
|
||||
let response = match http::create_client().get(&url).send().await {
|
||||
let response = match http::create_client()?.get(&url).send().await {
|
||||
Ok(response) => match response.json::<serde_json::Value>().await {
|
||||
Ok(json) => json,
|
||||
Err(err) => return Err(PolyError::JavaError(JavaDownloadError::NoJavaVersionFound(err.to_string())))
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
use std::{collections::HashMap, error::Error};
|
||||
|
||||
use anyhow::anyhow;
|
||||
use async_trait::async_trait;
|
||||
use serde_json::Value;
|
||||
use tauri::{AppHandle, Manager};
|
||||
use tokio::io::{stdout, AsyncWriteExt};
|
||||
|
||||
use crate::{create_client, create_manifest, game::client::{GameClient, GameClientDetails, GameClientType}, utils::http};
|
||||
use crate::{create_client, create_manifest, game::client::{GameClient, GameClientDetails, GameClientType}, utils::http, PolyError, PolyResult};
|
||||
|
||||
create_manifest!(VanillaManifest {
|
||||
// minecraft: vanilla_manifest::MinecraftManifest
|
||||
|
|
@ -22,14 +23,14 @@ impl GameClient for VanillaClient {
|
|||
}
|
||||
}
|
||||
|
||||
async fn get_all_version_urls() -> Result<HashMap<String, String>, Box<dyn Error>> {
|
||||
async fn get_all_version_urls() -> PolyResult<HashMap<String, String>> {
|
||||
const URL: &str = "https://piston-meta.mojang.com/mc/game/version_manifest_v2.json";
|
||||
let request = http::create_client().get(URL).send().await?;
|
||||
let request = http::create_client()?.get(URL).send().await.map_err(|err| PolyError::HTTPError(err))?;
|
||||
|
||||
let json = request.json::<Value>().await?;
|
||||
let json = request.json::<Value>().await.map_err(|err| PolyError::HTTPError(err))?;
|
||||
|
||||
let versions = json.get("versions").ok_or("No versions object")?
|
||||
.as_array().ok_or("Invalid versions object")?;
|
||||
let versions = json.get("versions").ok_or(PolyError::AnyhowError(anyhow!("failed to get versions object")))?
|
||||
.as_array().ok_or(PolyError::AnyhowError(anyhow!("invalid versions object")))?;
|
||||
|
||||
let mut map = HashMap::new();
|
||||
|
||||
|
|
@ -157,6 +158,7 @@ pub mod vanilla_impl {
|
|||
}
|
||||
}
|
||||
|
||||
// todo move to prisma
|
||||
pub mod vanilla_manifest {
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
|
@ -313,4 +315,4 @@ pub mod vanilla_manifest {
|
|||
pub url: String,
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ use self::client::get_game_client;
|
|||
pub mod client;
|
||||
mod clients;
|
||||
|
||||
// todo move to prisma
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum JavaVersion {
|
||||
|
|
@ -78,4 +79,4 @@ async fn set_selected_client(handle: tauri::AppHandle, state: tauri::State<'_, M
|
|||
let client = get_game_client(handle, details);
|
||||
state.selected_client = Some(client);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,9 @@ pub enum PolyError {
|
|||
#[error("a tauri runtime error occured (this should not happen): {0}")]
|
||||
/// Wrapper around [`tauri::Error`] to handle expected Tauri errors.
|
||||
TauriError(#[from] tauri::Error),
|
||||
#[error("failed to manage shell task: {0}")]
|
||||
/// Wrapper around [`tauri_plugin_shell::Error`] to handle shell errors.
|
||||
TauriShellError(#[from] tauri_plugin_shell::Error),
|
||||
#[error("failed to manage a tokio semaphore: {0}")]
|
||||
/// Wrapper around [`tokio::sync::AcquireError`] to handle sempahore errors.
|
||||
TokioError(#[from] tokio::sync::AcquireError),
|
||||
|
|
@ -42,9 +45,14 @@ pub enum PolyError {
|
|||
#[error("failed to download java: {0}")]
|
||||
/// Wrapper around [`game::client::JavaDownloadError`] to handle Java executable downloading.
|
||||
JavaError(#[from] game::client::JavaDownloadError),
|
||||
#[error("file management failed: {0}")]
|
||||
/// Wrapper around [`utils::file::FileError`] to handle file management errors.
|
||||
FileError(#[from] utils::file::FileError),
|
||||
/// Wrapper around [`zip::result::ZipError`] to handle zip errors.
|
||||
#[error("failed to manage zip files: {0}")]
|
||||
ZipError(#[from] zip::result::ZipError),
|
||||
#[error("failed to authenticate: {0}")]
|
||||
// Wrapper around [`auth::AuthenticationError`] to handle authentication errors.
|
||||
AuthError(#[from] auth::AuthenticationError),
|
||||
#[error(transparent)]
|
||||
AnyhowError(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
/// Alias for a [`Result`] with the error type [`PolyError`].
|
||||
|
|
|
|||
|
|
@ -1,32 +1,33 @@
|
|||
use std::{fs::File, path::Path};
|
||||
use anyhow::anyhow;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{PolyError, PolyResult};
|
||||
|
||||
pub fn extract_archive(archive: &Path, dest: &Path) -> PolyResult<()> {
|
||||
let ext = match archive.extension() {
|
||||
Some(ext) => ext.to_str().unwrap(),
|
||||
None => return Err(anyhow!("unsupported archive type"))
|
||||
None => return Err(PolyError::AnyhowError(anyhow!("unsupported operating system")))
|
||||
};
|
||||
|
||||
match ext {
|
||||
"zip" => extract_zip(archive, dest),
|
||||
"gz" => extract_tar_gz(archive, dest),
|
||||
_ => Err(PolyError::FileError(FileError::UnsupportedArch))
|
||||
_ => Err(PolyError::AnyhowError(anyhow!("unsupported file extension {:?}", ext)))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn extract_zip(archive: &Path, dest: &Path) -> PolyResult<()> {
|
||||
let mut archive = zip::ZipArchive::new(File::open(archive)?)
|
||||
.map_err(|err| PolyError::FileError(FileError::ZipError(err)));
|
||||
archive?.extract(dest).map_err(|err| PolyError::FileError(FileError::ZipError(err)));
|
||||
let file = File::open(archive).map_err(|err| PolyError::IOError(err))?;
|
||||
let mut archive = zip::ZipArchive::new(file)
|
||||
.map_err(|err| PolyError::ZipError(err));
|
||||
archive?.extract(dest).map_err(|err| PolyError::ZipError(err));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn extract_tar_gz(archive: &Path, dest: &Path) -> PolyResult<()> {
|
||||
let tar_gz = flate2::read::GzDecoder::new(File::open(archive)?);
|
||||
let file = File::open(archive).map_err(|err| PolyError::IOError(err))?;
|
||||
let tar_gz = flate2::read::GzDecoder::new(file);
|
||||
let mut archive = tar::Archive::new(tar_gz);
|
||||
archive.unpack(dest)?;
|
||||
archive.unpack(dest).map_err(|err| PolyError::IOError(err));
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,10 +10,11 @@ pub fn create_client() -> PolyResult<Client> {
|
|||
.build().map_err(|err| PolyError::HTTPError(err))
|
||||
}
|
||||
|
||||
// can we not make mapping the errors automatic, isnt that the whole point
|
||||
pub async fn download_file(url: &str, path: &Path) -> PolyResult<()> {
|
||||
let response = create_client()?.get(url).send().await?;
|
||||
let mut file = File::create(path)?;
|
||||
let mut content = Cursor::new(response.bytes().await?);
|
||||
io::copy(&mut content, &mut file)?;
|
||||
let response = create_client()?.get(url).send().await.map_err(|err| PolyError::HTTPError(err))?;
|
||||
let mut file = File::create(path).map_err(|err| PolyError::IOError(err))?;
|
||||
let mut content = Cursor::new(response.bytes().await.map_err(|err| PolyError::HTTPError(err))?);
|
||||
io::copy(&mut content, &mut file).map_err(|err| PolyError::IOError(err));
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue