Files
Rocketmc/theseus/src/state/mr_auth.rs
Geometrically afaec4b1bf Bump Theseus Version (#818)
* push to test on windows

* Fix windows sup

* Fix macos

* Fix back

* new resolver for windows testing

* Custom macos handling for some versions

* Fix 1.13+ broken

* fix arg parsing mac

* small winblows fix

* remove debug info; set meta url

* run lint + fix clippy

* Remove useless commnet
2023-10-21 13:08:47 -07:00

376 lines
9.8 KiB
Rust

use crate::config::MODRINTH_API_URL;
use crate::state::DirectoryInfo;
use crate::util::fetch::{
fetch_advanced, read_json, write, FetchSemaphore, IoSemaphore,
};
use crate::State;
use chrono::{DateTime, Duration, Utc};
use futures::TryStreamExt;
use reqwest::Method;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
const AUTH_JSON: &str = "auth.json";
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ModrinthUser {
pub id: String,
pub username: String,
pub name: Option<String>,
pub avatar_url: Option<String>,
pub bio: Option<String>,
pub created: DateTime<Utc>,
pub role: String,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ModrinthCredentials {
pub session: String,
pub expires_at: DateTime<Utc>,
pub user: ModrinthUser,
}
#[derive(Serialize)]
#[serde(tag = "type")]
#[serde(rename_all = "snake_case")]
pub enum ModrinthCredentialsResult {
TwoFactorRequired { flow: String },
Credentials(ModrinthCredentials),
}
#[derive(Debug)]
pub struct CredentialsStore(pub Option<ModrinthCredentials>);
impl CredentialsStore {
pub async fn init(
dirs: &DirectoryInfo,
io_semaphore: &IoSemaphore,
) -> crate::Result<Self> {
let auth_path = dirs.caches_meta_dir().await.join(AUTH_JSON);
let user = read_json(&auth_path, io_semaphore).await.ok();
if let Some(user) = user {
Ok(Self(Some(user)))
} else {
Ok(Self(None))
}
}
pub async fn save(&self) -> crate::Result<()> {
let state = State::get().await?;
let auth_path =
state.directories.caches_meta_dir().await.join(AUTH_JSON);
if let Some(creds) = &self.0 {
write(&auth_path, &serde_json::to_vec(creds)?, &state.io_semaphore)
.await?;
}
Ok(())
}
pub async fn login(
&mut self,
credentials: ModrinthCredentials,
) -> crate::Result<&Self> {
self.0 = Some(credentials);
self.save().await?;
Ok(self)
}
#[tracing::instrument]
pub async fn update_creds() {
let res = async {
let state = State::get().await?;
let mut creds_write = state.credentials.write().await;
refresh_credentials(&mut creds_write, &state.fetch_semaphore)
.await?;
Ok::<(), crate::Error>(())
}
.await;
match res {
Ok(()) => {}
Err(err) => {
tracing::warn!("Unable to update credentials: {err}")
}
};
}
pub async fn logout(&mut self) -> crate::Result<&Self> {
self.0 = None;
self.save().await?;
Ok(self)
}
}
pub struct ModrinthAuthFlow {
socket: async_tungstenite::WebSocketStream<
async_tungstenite::tokio::ConnectStream,
>,
}
impl ModrinthAuthFlow {
pub async fn new(provider: &str) -> crate::Result<Self> {
let (socket, _) = async_tungstenite::tokio::connect_async(format!(
"wss://api.modrinth.com/v2/auth/ws?provider={provider}"
))
.await?;
Ok(Self { socket })
}
pub async fn prepare_login_url(&mut self) -> crate::Result<String> {
let code_resp = self
.socket
.try_next()
.await?
.ok_or(
crate::ErrorKind::WSClosedError(String::from(
"login socket URL",
))
.as_error(),
)?
.into_data();
#[derive(Deserialize)]
struct Url {
url: String,
}
let response = serde_json::from_slice::<Url>(&code_resp)?;
Ok(response.url)
}
pub async fn extract_credentials(
&mut self,
semaphore: &FetchSemaphore,
) -> crate::Result<ModrinthCredentialsResult> {
// Minecraft bearer token
let token_resp = self
.socket
.try_next()
.await?
.ok_or(
crate::ErrorKind::WSClosedError(String::from(
"login socket URL",
))
.as_error(),
)?
.into_data();
let response =
serde_json::from_slice::<HashMap<String, Value>>(&token_resp)?;
get_result_from_res("code", response, semaphore).await
}
pub async fn close(&mut self) -> crate::Result<()> {
self.socket.close(None).await?;
Ok(())
}
}
async fn get_result_from_res(
code_key: &str,
response: HashMap<String, Value>,
semaphore: &FetchSemaphore,
) -> crate::Result<ModrinthCredentialsResult> {
if let Some(flow) = response.get("flow").and_then(|x| x.as_str()) {
Ok(ModrinthCredentialsResult::TwoFactorRequired {
flow: flow.to_string(),
})
} else if let Some(code) = response.get(code_key).and_then(|x| x.as_str()) {
let info = fetch_info(code, semaphore).await?;
Ok(ModrinthCredentialsResult::Credentials(
ModrinthCredentials {
session: code.to_string(),
expires_at: Utc::now() + Duration::weeks(2),
user: info,
},
))
} else if let Some(error) =
response.get("description").and_then(|x| x.as_str())
{
Err(crate::ErrorKind::OtherError(format!(
"Failed to login with error {error}"
))
.as_error())
} else {
Err(crate::ErrorKind::OtherError(String::from(
"Flow/code/error not found in response!",
))
.as_error())
}
}
#[derive(Deserialize)]
struct Session {
session: String,
}
pub async fn login_password(
username: &str,
password: &str,
challenge: &str,
semaphore: &FetchSemaphore,
) -> crate::Result<ModrinthCredentialsResult> {
let resp = fetch_advanced(
Method::POST,
&format!("{MODRINTH_API_URL}auth/login"),
None,
Some(serde_json::json!({
"username": username,
"password": password,
"challenge": challenge,
})),
None,
None,
semaphore,
&CredentialsStore(None),
)
.await?;
let value = serde_json::from_slice::<HashMap<String, Value>>(&resp)?;
get_result_from_res("session", value, semaphore).await
}
async fn get_creds_from_res(
response: HashMap<String, Value>,
semaphore: &FetchSemaphore,
) -> crate::Result<ModrinthCredentials> {
if let Some(code) = response.get("session").and_then(|x| x.as_str()) {
let info = fetch_info(code, semaphore).await?;
Ok(ModrinthCredentials {
session: code.to_string(),
expires_at: Utc::now() + Duration::weeks(2),
user: info,
})
} else if let Some(error) =
response.get("description").and_then(|x| x.as_str())
{
Err(crate::ErrorKind::OtherError(format!(
"Failed to login with error {error}"
))
.as_error())
} else {
Err(crate::ErrorKind::OtherError(String::from(
"Flow/code/error not found in response!",
))
.as_error())
}
}
pub async fn login_2fa(
code: &str,
flow: &str,
semaphore: &FetchSemaphore,
) -> crate::Result<ModrinthCredentials> {
let resp = fetch_advanced(
Method::POST,
&format!("{MODRINTH_API_URL}auth/login/2fa"),
None,
Some(serde_json::json!({
"code": code,
"flow": flow,
})),
None,
None,
semaphore,
&CredentialsStore(None),
)
.await?;
let response = serde_json::from_slice::<HashMap<String, Value>>(&resp)?;
get_creds_from_res(response, semaphore).await
}
pub async fn create_account(
username: &str,
email: &str,
password: &str,
challenge: &str,
sign_up_newsletter: bool,
semaphore: &FetchSemaphore,
) -> crate::Result<ModrinthCredentials> {
let resp = fetch_advanced(
Method::POST,
&format!("{MODRINTH_API_URL}auth/create"),
None,
Some(serde_json::json!({
"username": username,
"email": email,
"password": password,
"challenge": challenge,
"sign_up_newsletter": sign_up_newsletter,
})),
None,
None,
semaphore,
&CredentialsStore(None),
)
.await?;
let response = serde_json::from_slice::<HashMap<String, Value>>(&resp)?;
get_creds_from_res(response, semaphore).await
}
pub async fn refresh_credentials(
credentials_store: &mut CredentialsStore,
semaphore: &FetchSemaphore,
) -> crate::Result<()> {
if let Some(ref mut credentials) = credentials_store.0 {
let token = &credentials.session;
let resp = fetch_advanced(
Method::POST,
&format!("{MODRINTH_API_URL}session/refresh"),
None,
None,
Some(("Authorization", token)),
None,
semaphore,
&CredentialsStore(None),
)
.await
.ok()
.and_then(|resp| serde_json::from_slice::<Session>(&resp).ok());
if let Some(value) = resp {
credentials.user = fetch_info(&value.session, semaphore).await?;
credentials.session = value.session;
credentials.expires_at = Utc::now() + Duration::weeks(2);
} else if credentials.expires_at < Utc::now() {
credentials_store.0 = None;
}
}
Ok(())
}
async fn fetch_info(
token: &str,
semaphore: &FetchSemaphore,
) -> crate::Result<ModrinthUser> {
let result = fetch_advanced(
Method::GET,
&format!("{MODRINTH_API_URL}user"),
None,
None,
Some(("Authorization", token)),
None,
semaphore,
&CredentialsStore(None),
)
.await?;
let value = serde_json::from_slice(&result)?;
Ok(value)
}