From dc3917b05018cb32e2451d9eaed242036c5e7512 Mon Sep 17 00:00:00 2001 From: bigfoot547 Date: Tue, 28 Jan 2025 21:23:07 -0600 Subject: wip: auth --- Cargo.lock | 21 ++++++++++ Cargo.toml | 4 +- src/auth.rs | 54 +++++++++++++++++++++++- src/auth/device_code.rs | 103 ++++++++++++++++++++++++++++++++++++++++++++++ src/auth/types.rs | 49 ++++++++++++++++++++++ src/launcher/constants.rs | 7 +--- src/launcher/download.rs | 3 +- src/lib.rs | 2 +- src/util.rs | 7 ++++ 9 files changed, 239 insertions(+), 11 deletions(-) create mode 100644 src/auth/device_code.rs create mode 100644 src/auth/types.rs diff --git a/Cargo.lock b/Cargo.lock index 4cf2006..f9091a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3341,6 +3341,15 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "multimap" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03" +dependencies = [ + "serde", +] + [[package]] name = "native-tls" version = "0.2.12" @@ -3490,6 +3499,7 @@ dependencies = [ "lazy_static", "log", "lzma-rs", + "multimap", "regex", "reqwest", "serde", @@ -3498,6 +3508,7 @@ dependencies = [ "sysinfo", "tokio", "tokio-stream", + "uuid", "walkdir", "zip", ] @@ -5896,6 +5907,16 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "uuid" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3758f5e68192bb96cc8f9b7e2c2cfdabb435499a28499a42f8f984092adad4b" +dependencies = [ + "getrandom 0.2.15", + "serde", +] + [[package]] name = "vcpkg" version = "0.2.15" diff --git a/Cargo.toml b/Cargo.toml index 0b15061..d4a405c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,14 +12,16 @@ indexmap = { version = "2.7.1", features = ["serde"] } lazy_static = "1.5.0" log = "0.4.22" lzma-rs = { version = "0.3.0", features = ["stream"] } +multimap = { version = "0.10.0", features = ["serde"] } regex = "1.11.1" reqwest = { version = "0.12.12", features = ["json", "stream"] } serde = { version = "1.0.216", features = ["derive"] } serde_json = "1.0.133" sha1_smol = { version = "1.0.1", features = ["alloc", "std", "serde"] } sysinfo = { version = "0.33.1", features = ["system", "multithread"] } -tokio = { version = "1.42.0", features = ["fs", "io-util", "sync", "rt"] } +tokio = { version = "1.42.0", features = ["fs", "io-util", "sync", "rt", "macros"] } tokio-stream = { version = "0.1.17", features = ["fs"] } +uuid = { version = "1.12.1", features = ["v4", "serde"] } walkdir = "2.5.0" zip = { version = "2.2.2", default-features = false, features = ["bzip2", "deflate", "deflate64", "lzma", "xz"] } diff --git a/src/auth.rs b/src/auth.rs index a11c0de..f4522ed 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1,3 +1,55 @@ -struct AuthenticationDatabase { +mod types; +pub mod device_code; +use std::error::Error; +use std::fmt::{Display, Formatter}; +use chrono::{DateTime, Utc}; +pub use types::*; + +#[derive(Debug)] +pub enum AuthError { + Request { what: &'static str, error: reqwest::Error }, +} + +impl Display for AuthError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + AuthError::Request { what, error } => write!(f, "auth request error ({}): {}", what, error) + } + } +} + +impl Error for AuthError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + AuthError::Request { error, .. } => Some(error) + } + } +} + +impl Token { + fn is_expired(&self, now: DateTime) -> bool { + self.expire.is_some_and(|exp| now < exp) + } +} + +impl MsaUser { + async fn log_in(&mut self) -> Result<(), AuthError> { + todo!() + } +} + +#[cfg(test)] +mod test { + use reqwest::Client; + use super::*; + + #[tokio::test] + async fn abc() { + device_code::DeviceCodeAuthBuilder::new() + .client_id("00000000402b5328") + .scope("service::user.auth.xboxlive.com::MBI_SSL") + .url("https://login.live.com/oauth20_connect.srf") + .begin(Client::new()).await.unwrap(); + } } diff --git a/src/auth/device_code.rs b/src/auth/device_code.rs new file mode 100644 index 0000000..087ff27 --- /dev/null +++ b/src/auth/device_code.rs @@ -0,0 +1,103 @@ +use std::ops::Add; +use std::time::Duration; +use futures::TryFutureExt; +use reqwest::Client; +use serde::{Deserialize, Serialize}; +use tokio::time::{Instant, MissedTickBehavior}; +use super::AuthError; +use crate::util::USER_AGENT; + +pub struct DeviceCodeAuthBuilder { + client_id: Option, + scope: Option, + url: Option +} + +#[derive(Serialize, Debug)] +struct DeviceCodeRequest { + client_id: String, + scope: String, + response_type: String +} + +#[derive(Deserialize, Debug)] +struct DeviceCodeResponse { + device_code: String, + user_code: String, + verification_uri: String, + expires_in: u64, + interval: u64, + message: Option +} + +impl DeviceCodeAuthBuilder { + pub fn new() -> DeviceCodeAuthBuilder { + DeviceCodeAuthBuilder { + client_id: None, + scope: None, + url: None + } + } + + pub fn client_id(mut self, client_id: &str) -> Self { + self.client_id = Some(client_id.to_owned()); + self + } + + pub fn scope(mut self, scope: &str) -> Self { + self.scope = Some(scope.to_owned()); + self + } + + pub fn url(mut self, url: &str) -> Self { + self.url = Some(url.to_owned()); + self + } + + pub async fn begin(self, client: Client) -> Result { + let scope = self.scope.expect("scope is not optional"); + let client_id = self.client_id.expect("client_id is not optional"); + let url = self.url.expect("url is not optional"); + + let device_code: DeviceCodeResponse = client.post(&url) + .header(reqwest::header::USER_AGENT, USER_AGENT) + .header(reqwest::header::ACCEPT, "application/json") + .form(&DeviceCodeRequest { + client_id, + scope, + response_type: "device_code".into() + }) + .send().await + .and_then(|r| r.error_for_status()) + .map_err(|e| AuthError::Request { what: "requesting device code auth", error: e })? + .json().await.map_err(|e| AuthError::Request { what: "receiving device code auth", error: e })?; + + let now = Instant::now(); + Ok(DeviceCodeAuth { + client, + start: now, + interval: Duration::from_secs(device_code.interval + 1), + expire_time: now.add(Duration::from_secs(device_code.expires_in)), + info: dbg!(device_code) + }) + } +} + +pub struct DeviceCodeAuth { + client: Client, + start: Instant, + interval: Duration, + expire_time: Instant, + info: DeviceCodeResponse +} + +impl DeviceCodeAuth { + async fn drive(&self) { + let mut i = tokio::time::interval_at(self.start, self.interval); + i.set_missed_tick_behavior(MissedTickBehavior::Skip); + + while self.expire_time.elapsed().is_zero() { + + } + } +} diff --git a/src/auth/types.rs b/src/auth/types.rs new file mode 100644 index 0000000..8889b63 --- /dev/null +++ b/src/auth/types.rs @@ -0,0 +1,49 @@ +use chrono::{DateTime, Utc}; +use multimap::MultiMap; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Property { + pub value: String, + pub signature: Option +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct UserProfile { + pub uuid: Option, + pub name: Option, + + #[serde(default, skip_serializing_if = "MultiMap::is_empty")] + pub properties: MultiMap +} + +#[derive(Debug, Serialize, Deserialize)] +pub(super) struct Token { + pub value: String, + + #[serde(skip_serializing_if = "Option::is_none")] + pub expire: Option> +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct MsaUser { + #[serde(skip_serializing_if = "Option::is_none")] + pub profile: Option, + pub xuid: Uuid, + pub(super) auth_token: Option, + pub(super) xbl_token: Option, + pub(super) refresh_token: Option +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(tag = "type")] +pub enum User { + Dummy(UserProfile), + MSA(MsaUser) +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct AuthenticationDatabase { + pub users: Vec +} diff --git a/src/launcher/constants.rs b/src/launcher/constants.rs index aba0650..db90d2f 100644 --- a/src/launcher/constants.rs +++ b/src/launcher/constants.rs @@ -1,12 +1,6 @@ -use const_format::formatcp; use lazy_static::lazy_static; use regex::Regex; -const PKG_NAME: &str = env!("CARGO_PKG_NAME"); -const PKG_VERSION: &str = env!("CARGO_PKG_VERSION"); -const CRATE_NAME: &str = env!("CARGO_CRATE_NAME"); - -pub const USER_AGENT: &str = formatcp!("{PKG_NAME}/{PKG_VERSION} (in {CRATE_NAME})"); pub const URL_VERSION_MANIFEST: &str = "https://piston-meta.mojang.com/mc/game/version_manifest_v2.json"; pub const URL_RESOURCE_BASE: &str = "https://resources.download.minecraft.net/"; pub const URL_JRE_MANIFEST: &str = "https://piston-meta.mojang.com/v1/products/java-runtime/2ec0cc96c44e5a76b9c8b7c39df7210883d12871/all.json"; @@ -16,6 +10,7 @@ pub const NATIVES_PREFIX: &str = "natives-"; pub const DEF_INSTANCE_NAME: &'static str = "default"; pub const DEF_PROFILE_NAME: &'static str = "default"; +// 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 lazy_static! { diff --git a/src/launcher/download.rs b/src/launcher/download.rs index ec4a59c..3a89d79 100644 --- a/src/launcher/download.rs +++ b/src/launcher/download.rs @@ -8,9 +8,8 @@ use sha1_smol::{Digest, Sha1}; use tokio::fs; use tokio::fs::File; use tokio::io::{self, AsyncWriteExt}; -use crate::launcher::constants::USER_AGENT; use crate::util; -use crate::util::{FileVerifyError, IntegrityError}; +use crate::util::{FileVerifyError, IntegrityError, USER_AGENT}; pub trait Download: Debug + Display { // return Ok(None) to skip downloading this file diff --git a/src/lib.rs b/src/lib.rs index 05c0c7e..0d2233b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,4 +2,4 @@ mod util; pub mod version; pub mod assets; pub mod launcher; -mod auth; +pub mod auth; // temporarily public diff --git a/src/util.rs b/src/util.rs index 0685848..8d35fb9 100644 --- a/src/util.rs +++ b/src/util.rs @@ -2,6 +2,7 @@ use std::error::Error; use std::fmt::{Display, Formatter}; use std::io::ErrorKind; use std::path::{Component, Path, PathBuf}; +use const_format::formatcp; use log::{debug, info, warn}; use sha1_smol::{Digest, Sha1}; use tokio::fs::File; @@ -9,6 +10,12 @@ use tokio::{fs, io}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use crate::util; +const PKG_NAME: &str = env!("CARGO_PKG_NAME"); +const PKG_VERSION: &str = env!("CARGO_PKG_VERSION"); +const CRATE_NAME: &str = env!("CARGO_CRATE_NAME"); + +pub const USER_AGENT: &str = formatcp!("{PKG_NAME}/{PKG_VERSION} (in {CRATE_NAME})"); + #[derive(Debug)] pub enum IntegrityError { SizeMismatch{ expect: usize, actual: usize }, -- cgit v1.2.3-70-g09d2