From 703a2727e788506446c7038a2a1750c5e3458f7c Mon Sep 17 00:00:00 2001 From: shiroyashik Date: Sat, 15 Jun 2024 19:27:50 +0300 Subject: [PATCH] The user system has been refactored --- Config.example.toml | 5 +- src/auth.rs | 108 ++++++++++++++++++++++++++++++++++++++------ src/main.rs | 81 +++++++++------------------------ src/profile.rs | 8 ++-- src/utils.rs | 20 +++++++- src/ws/handler.rs | 14 +++--- 6 files changed, 149 insertions(+), 87 deletions(-) diff --git a/Config.example.toml b/Config.example.toml index ce64829..74ae408 100644 --- a/Config.example.toml +++ b/Config.example.toml @@ -44,13 +44,16 @@ motd = """ maxAvatarSize = 100000 maxAvatars = 10 -# Shiroyashik [advancedUsers.66004548-4de5-49de-bade-9c3933d8eb97] +username = "Shiroyashik" +authSystem = "elyby" special = [0,0,0,1,0,0] # 6 pride = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] # 25 # Your nickname here # [advancedUsers.your uuid here] +# username = "Bot" +# authSystem = "mojang" # can be: mojang, elyby, internal (cant be authenticated) # special = [0,1,0,0,0,0] # and set badges what you want! :D # pride = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] diff --git a/src/auth.rs b/src/auth.rs index 06b2c98..21e5649 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1,3 +1,5 @@ +use std::{str::FromStr, sync::Arc}; + use crate::utils::*; use anyhow::anyhow; use axum::{ @@ -8,6 +10,7 @@ use axum::{ routing::get, Router, }; +use dashmap::DashMap; use ring::digest::{self, digest}; use serde::Deserialize; use tracing::{debug, error, info, trace}; @@ -35,8 +38,8 @@ async fn id( ) -> String { let server_id = hex::encode(&digest(&digest::SHA1_FOR_LEGACY_USE_ONLY, &rand()).as_ref()[0..20]); - let state = state.pending; - state.insert(server_id.clone(), query.username); + let state = state.user_manager; + state.pending_insert(server_id.clone(), query.username); server_id } @@ -52,7 +55,7 @@ async fn verify( State(state): State, ) -> Response { let server_id = query.id.clone(); - let username = state.pending.remove(&server_id).unwrap().1; // TODO: Add error check + let username = state.user_manager.pending_remove(&server_id).unwrap().1; // TODO: Add error check let userinfo = match has_joined(&server_id, &username).await { Ok(d) => d, Err(e) => { @@ -62,14 +65,15 @@ async fn verify( }; if let Some((uuid, auth_system)) = userinfo { info!("[Authentication] {username} logged in using {auth_system:?}"); - let authenticated = state.authenticated; + let authenticated = state.user_manager; authenticated.insert( uuid, server_id.clone(), - crate::Userinfo { + Userinfo { username, uuid, auth_system, + token: Some(server_id.clone()), }, ); (StatusCode::OK, server_id.to_string()).into_response() @@ -82,7 +86,7 @@ async fn verify( pub async fn status(Token(token): Token, State(state): State) -> Response { match token { Some(token) => { - if state.authenticated.contains_token(&token) { + if state.user_manager.is_authenticated(&token) { (StatusCode::OK, "ok".to_string()).into_response() } else { @@ -121,9 +125,9 @@ where } // End Extractor -// Work with external APIs -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Deserialize)] pub enum AuthSystem { + Internal, ElyBy, Mojang, } @@ -131,21 +135,37 @@ pub enum AuthSystem { impl ToString for AuthSystem { fn to_string(&self) -> String { match self { + AuthSystem::Internal => String::from("internal"), AuthSystem::ElyBy => String::from("elyby"), AuthSystem::Mojang => String::from("mojang"), } } } +impl FromStr for AuthSystem { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + match s { + "internal" => Ok(Self::Internal), + "elyby" => Ok(Self::ElyBy), + "mojang" => Ok(Self::Mojang), + _ => Err(anyhow!("No auth system called: {s}")) + } + } +} + impl AuthSystem { fn get_url(&self) -> String { match self { + AuthSystem::Internal => panic!("Can't get internal URL!"), AuthSystem::ElyBy => String::from("http://minecraft.ely.by/session/hasJoined"), AuthSystem::Mojang => String::from("https://sessionserver.mojang.com/session/minecraft/hasJoined"), } } } +// Work with external APIs /// Get UUID from JSON response #[inline] fn get_id_json(json: &serde_json::Value) -> anyhow::Result { @@ -199,11 +219,71 @@ pub async fn has_joined( } else { panic!("Impossible error!") } - // FOR DELETE - // tokio::select! { - // Ok(res) = tokio::spawn(fetch_json(AuthSystem::ElyBy, server_id, username)) => {Ok(res)}, - // Ok(res) = tokio::spawn(fetch_json(AuthSystem::Mojang, server_id, username)) => {Ok(res)}, - // else => {Err(anyhow!("Something went wrong in external apis request process"))} - // } } // End of work with external APIs + +// User manager +#[derive(Debug, Clone)] +pub struct UManager { + /// Users with incomplete authentication + pending: Arc>, // TODO: Add automatic purge + /// Authenticated users + authenticated: Arc>, // NOTE: In the future, try it in a separate LockRw branch + /// Registered users + registered: Arc>, +} + +#[derive(Debug, Clone)] +pub struct Userinfo { + pub username: String, + pub uuid: Uuid, + pub auth_system: AuthSystem, + pub token: Option, +} + +impl UManager { + pub fn new() -> Self { + Self { + pending: Arc::new(DashMap::new()), + registered: Arc::new(DashMap::new()), + authenticated: Arc::new(DashMap::new()), + } + } + pub fn pending_insert(&self, server_id: String, username: String) { + self.pending.insert(server_id, username); + } + pub fn pending_remove(&self, server_id: &str) -> std::option::Option<(std::string::String, std::string::String)> { + self.pending.remove(server_id) + } + pub fn insert(&self, uuid: Uuid, token: String, userinfo: Userinfo) -> Option { + self.authenticated.insert(token, uuid); + self.registered.insert(uuid, userinfo) + } + pub fn insert_user(&self, uuid: Uuid, userinfo: Userinfo) -> Option { + self.registered.insert(uuid, userinfo) + } + pub fn get( + &self, + token: &String, + ) -> Option> { + let uuid = self.authenticated.get(token)?; + self.registered.get(uuid.value()) + } + pub fn get_by_uuid( + &self, + uuid: &Uuid, + ) -> Option> { + self.registered.get(uuid) + } + pub fn is_authenticated(&self, token: &String) -> bool { + self.authenticated.contains_key(token) + } + pub fn is_registered(&self, uuid: &Uuid) -> bool { + self.registered.contains_key(uuid) + } + pub fn remove(&self, uuid: &Uuid) { + let token = self.registered.remove(uuid).unwrap().1.token.unwrap(); + self.authenticated.remove(&token); + } +} +// End of User manager \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index c31c69d..50dcba6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ use axum::{ extract::DefaultBodyLimit, middleware::from_extractor, routing::{delete, get, post, put}, Router }; use dashmap::DashMap; +use utils::collect_advanced_users; use std::sync::Arc; use tokio::sync::{broadcast, Mutex}; use tower_http::trace::TraceLayer; @@ -15,7 +16,7 @@ use ws::handler; // API: Auth mod auth; -use auth as api_auth; +use auth::{self as api_auth, UManager}; // API: Server info mod info; @@ -31,64 +32,17 @@ mod utils; // Config mod config; -#[derive(Debug, Clone)] -pub struct Userinfo { - username: String, - uuid: Uuid, - auth_system: api_auth::AuthSystem, -} - -#[derive(Debug, Clone)] -struct Authenticated { - user_data: DashMap, - uuid: DashMap, -} - -impl Authenticated { - fn new() -> Self { - Self { - user_data: DashMap::new(), - uuid: DashMap::new(), - } - } - pub fn insert(&self, uuid: Uuid, token: String, userinfo: Userinfo) -> Option { - self.uuid.insert(uuid, token.clone()); - self.user_data.insert(token, userinfo) - } - pub fn get( - &self, - token: &String, - ) -> Option> { - self.user_data.get(token) - } - pub fn get_by_uuid( - &self, - uuid: &Uuid, - ) -> Option> { - if let Some(token) = self.uuid.get(uuid) { - self.user_data.get(&token.clone()) - } else { - None - } - } - pub fn contains_token(&self, token: &String) -> bool { - self.user_data.contains_key(token) - } - pub fn remove(&self, uuid: &Uuid) { - let token = self.uuid.remove(uuid).unwrap().1; - self.user_data.remove(&token); - } -} - #[derive(Debug, Clone)] pub struct AppState { - // Users with incomplete authentication - pending: Arc>, // - // Authenticated users - authenticated: Arc, // NOTE: In the future, try it in a separate LockRw branch - // Ping broadcasts for WebSocket connections + /// Users with incomplete authentication + //pending: Arc>, // + /// Authenticated users + //authenticated: Arc, // NOTE: In the future, try it in a separate LockRw branch + /// User manager + user_manager: Arc, + /// Ping broadcasts for WebSocket connections broadcasts: Arc>>>, - // Current configuration + /// Current configuration config: Arc>, } @@ -117,25 +71,32 @@ async fn main() -> Result<()> { // State let state = AppState { - pending: Arc::new(DashMap::new()), - authenticated: Arc::new(Authenticated::new()), + user_manager: Arc::new(UManager::new()), broadcasts: Arc::new(DashMap::new()), config: config, }; // Automatic update of configuration while the server is running let config_update = state.config.clone(); + let user_manager = Arc::clone(&state.user_manager); tokio::spawn(async move { loop { - tokio::time::sleep(std::time::Duration::from_secs(10)).await; - let new_config = config::Config::parse(config_file.clone().into()); let mut config = config_update.lock().await; if new_config != *config { info!("Server configuration modification detected!"); *config = new_config; + // let collected = collect_advanced_users(&config.advanced_users); + // for (uuid, userinfo) in collected { + // user_manager.insert_user(uuid, userinfo); + // } } + let collected = collect_advanced_users(&config.advanced_users); + for (uuid, userinfo) in collected { + user_manager.insert_user(uuid, userinfo); + } + tokio::time::sleep(std::time::Duration::from_secs(10)).await; } }); diff --git a/src/profile.rs b/src/profile.rs index 4832139..d377cf2 100644 --- a/src/profile.rs +++ b/src/profile.rs @@ -29,7 +29,7 @@ pub async fn user_info( let avatar_file = format!("avatars/{}.moon", formatted_uuid); - let auth_system = match state.authenticated.get_by_uuid(&uuid) { + let auth_system = match state.user_manager.get_by_uuid(&uuid) { Some(d) => d.auth_system.to_string(), None => return Json(json!("err")), }; @@ -113,7 +113,7 @@ pub async fn upload_avatar( None => http_error_ret!(UNAUTHORIZED, "Authentication error!"), }; - if let Some(user_info) = state.authenticated.get(&token) { + if let Some(user_info) = state.user_manager.get(&token) { tracing::info!( "{} ({}) trying to upload an avatar", user_info.uuid, @@ -128,7 +128,7 @@ pub async fn upload_avatar( pub async fn equip_avatar(Token(token): Token, State(state): State) -> String { debug!("[API] S2C : Equip"); - let uuid = state.authenticated.get(&token.unwrap()).unwrap().uuid; + let uuid = state.user_manager.get(&token.unwrap()).unwrap().uuid; if state .broadcasts .get(&uuid) @@ -147,7 +147,7 @@ pub async fn delete_avatar(Token(token): Token, State(state): State) - Some(t) => t, None => http_error_ret!(UNAUTHORIZED, "Authentication error!"), }; - if let Some(user_info) = state.authenticated.get(&token) { + if let Some(user_info) = state.user_manager.get(&token) { tracing::info!( "{} ({}) is trying to delete the avatar", user_info.uuid, diff --git a/src/utils.rs b/src/utils.rs index 62c9f82..3816abe 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,10 +1,12 @@ -use std::{fs::File, io::Read}; +use std::{fs::File, io::Read, str::FromStr}; use base64::prelude::*; use rand::{distributions::Alphanumeric, thread_rng, Rng}; use ring::digest::{self, digest}; use uuid::Uuid; +use crate::auth::{AuthSystem, Userinfo}; + // Core functions pub fn rand() -> [u8; 50] { let mut rng = thread_rng(); @@ -34,6 +36,22 @@ pub fn get_correct_array(value: &toml::Value) -> Vec { .map(move |x| x.as_integer().unwrap() as u8) .collect() } +pub fn collect_advanced_users(value: &toml::Table) -> Vec<(Uuid, Userinfo)> { + value + .iter() + .map( |(uuid, userdata)| { + let auth_system = AuthSystem::from_str(userdata.as_table().unwrap().get("authSystem").expect("Can't find authSystem in advancedUser!").as_str().unwrap()).unwrap(); + let username = userdata.as_table().unwrap().get("username").expect("Can't find username in advancedUser!").as_str().unwrap().to_string(); + ( + Uuid::parse_str(uuid).unwrap(), + Userinfo { username, + uuid: Uuid::parse_str(uuid).unwrap(), + auth_system, + token: None + } + )}) + .collect() +} pub fn format_uuid(uuid: &Uuid) -> String { // let uuid = Uuid::parse_str(&uuid)?; TODO: Вероятно format_uuid стоит убрать diff --git a/src/ws/handler.rs b/src/ws/handler.rs index 126151e..0e54f51 100644 --- a/src/ws/handler.rs +++ b/src/ws/handler.rs @@ -59,7 +59,7 @@ async fn handle_socket(mut socket: WebSocket, state: AppState) { info!("[WebSocket{}] Connection successfully closed!", owner.name()); if let Some(u) = owner.0 { state.broadcasts.remove(&u.uuid); - state.authenticated.remove(&u.uuid); + state.user_manager.remove(&u.uuid); } return; } @@ -68,7 +68,7 @@ async fn handle_socket(mut socket: WebSocket, state: AppState) { debug!("[WebSocket{}] Receive error! Connection terminated!", owner.name()); if let Some(u) = owner.0 { state.broadcasts.remove(&u.uuid); - state.authenticated.remove(&u.uuid); + state.user_manager.remove(&u.uuid); } return; }; @@ -82,7 +82,7 @@ async fn handle_socket(mut socket: WebSocket, state: AppState) { error!("[WebSocket{}] This message is not from Figura! {e:?}", owner.name()); if let Some(u) = owner.0 { state.broadcasts.remove(&u.uuid); - state.authenticated.remove(&u.uuid); + state.user_manager.remove(&u.uuid); } return; }, @@ -94,7 +94,7 @@ async fn handle_socket(mut socket: WebSocket, state: AppState) { C2SMessage::Token(token) => { debug!("[WebSocket{}] C2S : Token", owner.name()); let token = String::from_utf8(token.to_vec()).unwrap(); - match state.authenticated.get(&token) { // The principle is simple: if there is no token in authenticated, then it's "dirty hacker" :D + match state.user_manager.get(&token) { // The principle is simple: if there is no token in authenticated, then it's "dirty hacker" :D Some(t) => { //username = t.username.clone(); owner.0 = Some(WSUser { username: t.username.clone(), token, uuid: t.uuid }); @@ -115,7 +115,7 @@ async fn handle_socket(mut socket: WebSocket, state: AppState) { debug!("[WebSocket] Tried to log in with {token}"); // Tried to log in with token: {token} if let Some(u) = owner.0 { state.broadcasts.remove(&u.uuid); - state.authenticated.remove(&u.uuid); + state.user_manager.remove(&u.uuid); } return; // TODO: Define the trip code }, @@ -170,7 +170,7 @@ async fn handle_socket(mut socket: WebSocket, state: AppState) { warn!("[WebSocket{}] Send error! Connection terminated!", owner.name()); if let Some(u) = owner.0 { state.broadcasts.remove(&u.uuid); - state.authenticated.remove(&u.uuid); + state.user_manager.remove(&u.uuid); } return; } @@ -184,7 +184,7 @@ async fn handle_socket(mut socket: WebSocket, state: AppState) { warn!("[WebSocketSubscriber{}] Send error! Connection terminated!", owner.name()); if let Some(u) = owner.0 { state.broadcasts.remove(&u.uuid); - state.authenticated.remove(&u.uuid); + state.user_manager.remove(&u.uuid); } return; }