From dc3917b05018cb32e2451d9eaed242036c5e7512 Mon Sep 17 00:00:00 2001 From: bigfoot547 Date: Tue, 28 Jan 2025 21:23:07 -0600 Subject: wip: auth --- 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 ++++ 7 files changed, 215 insertions(+), 10 deletions(-) create mode 100644 src/auth/device_code.rs create mode 100644 src/auth/types.rs (limited to 'src') 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