diff --git a/.gitignore b/.gitignore index 9fccdd2..cc1073d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,10 @@ /target /Assets-main /avatars +/logs output.log docker-compose.yml Config.toml -.env \ No newline at end of file +.env +perf.data* +banned-players.json \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 16d41eb..05f9338 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -47,21 +47,6 @@ version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" -[[package]] -name = "anyhow-http" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1e146d0ff1e765ca855fba9205903fd26a0f67956be6c2a2b3b1dddbe62c182" -dependencies = [ - "anyhow", - "axum", - "bytes", - "http", - "mime", - "serde", - "serde_json", -] - [[package]] name = "async-trait" version = "0.1.81" @@ -380,6 +365,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.20" @@ -426,6 +420,15 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + [[package]] name = "digest" version = "0.10.7" @@ -1028,6 +1031,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-traits" version = "0.2.19" @@ -1184,6 +1193,12 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -1481,10 +1496,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "sculptor" -version = "0.2.3" +version = "0.3.0-dev" dependencies = [ "anyhow", - "anyhow-http", "axum", "base64 0.22.1", "chrono", @@ -1498,10 +1512,13 @@ dependencies = [ "semver", "serde", "serde_json", + "thiserror", "tokio", "toml 0.8.16", "tower-http", "tracing", + "tracing-appender", + "tracing-panic", "tracing-subscriber", "uuid", ] @@ -1812,6 +1829,37 @@ dependencies = [ "once_cell", ] +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinyvec" version = "1.8.0" @@ -1829,9 +1877,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.39.1" +version = "1.39.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d040ac2b29ab03b09d4129c2f5bbd012a3ac2f79d38ff506a4bf8dd34b0eac8a" +checksum = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1" dependencies = [ "backtrace", "bytes", @@ -2002,6 +2050,18 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-appender" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" +dependencies = [ + "crossbeam-channel", + "thiserror", + "time", + "tracing-subscriber", +] + [[package]] name = "tracing-attributes" version = "0.1.27" @@ -2034,6 +2094,16 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-panic" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bf1298a179837099f9309243af3b554e840f7f67f65e9f55294913299bd4cc5" +dependencies = [ + "tracing", + "tracing-subscriber", +] + [[package]] name = "tracing-subscriber" version = "0.3.18" diff --git a/Cargo.toml b/Cargo.toml index 89fefbe..0c8b5b2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,20 +1,20 @@ [package] name = "sculptor" authors = ["Shiroyashik "] -version = "0.2.3" +version = "0.3.0-dev" edition = "2021" publish = false [dependencies] # Logging tracing-subscriber = { version = "0.3.18", features = ["env-filter", "chrono"] } +tracing-appender = "0.2.3" +tracing-panic = "0.1.2" tracing = "0.1.40" # Errors handelers anyhow = "1.0.83" -anyhow-http = { version = "0.3.0", features = ["axum"] } - -# Serialization +thiserror = "1.0.63" chrono = { version = "0.4.38", features = ["now", "serde"] } serde = { version = "1.0.201", features = ["derive"] } serde_json = "1.0.117" diff --git a/Config.example.toml b/Config.example.toml index 9124de6..8799d34 100644 --- a/Config.example.toml +++ b/Config.example.toml @@ -4,9 +4,25 @@ listen = "0.0.0.0:6665" ## Don't touch if you don't know what you're doing # token = "" +## Path to minecraft server folder +## Sculptor try to use ban list from it +# mcFolder = "~/minecraft_server" + +## Can't work without at least one provider! +## If not set, default providers (Mojang, ElyBy) will be provided. +# authProviders = [ +# { name = "Mojang", url = "https://sessionserver.mojang.com/session/minecraft/hasJoined" }, +# { name = "ElyBy", url = "http://minecraft.ely.by/session/hasJoined" }, +# ] + ## Message of The Day ## It will be displayed to every player in the Figura menu who is connected to your server -motd = """ +[motd] +displayServerInfo = true +sInfoUptime = "Uptime: " +sInfoAuthClients = "Authenticated clients: " +sInfoDrawIndent = true +customText = """ [ { "text": "You are connected to " @@ -48,18 +64,15 @@ motd = """ maxAvatarSize = 100000 # 100 KB maxAvatars = 10 -[advancedUsers] - -# [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 +[advancedUsers.66004548-4de5-49de-bade-9c3933d8eb97] +username = "Shiroyashik" +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 ## With advancedUsers you can set additional parameters # [advancedUsers.your uuid here] # username = "Your_username_here" -# authSystem = "mojang" # can be: mojang, elyby, internal (cant be authenticated) +# banned = true # 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/note.txt b/note.txt index d91f1d0..1bbdfe6 100644 --- a/note.txt +++ b/note.txt @@ -52,4 +52,10 @@ Pride badges 22 Плюрал 23 Полисексуал 24 Прайд -25 Трансгендер \ No newline at end of file +25 Трансгендер + +Toast +0 Blue (Default) +1 Yellow (Warning) +2 Red (Error) +3 Cookie! (OwO) \ No newline at end of file diff --git a/src/api/errors.rs b/src/api/errors.rs new file mode 100644 index 0000000..785d922 --- /dev/null +++ b/src/api/errors.rs @@ -0,0 +1,41 @@ +use axum::{http::StatusCode, response::{IntoResponse, Response}}; +use thiserror::Error; +use tracing::{error, warn}; + +pub type ApiResult = Result; + +#[derive(Error, Debug)] +pub enum ApiError { + #[error("bad request")] + BadRequest, // 400 + #[error("unauthorized")] + Unauthorized, // 401 + #[error("not found")] + NotFound, // 404 + #[error("not acceptable")] + NotAcceptable, // 406 + #[error("internal server error")] + Internal, // 500 +} + +impl IntoResponse for ApiError { + fn into_response(self) -> Response { + match self { + ApiError::BadRequest => (StatusCode::BAD_REQUEST, "bad request").into_response(), + ApiError::Unauthorized => (StatusCode::UNAUTHORIZED, "unauthorized").into_response(), + ApiError::NotAcceptable=> (StatusCode::NOT_ACCEPTABLE, "not acceptable").into_response(), + ApiError::NotFound => (StatusCode::NOT_FOUND, "not found").into_response(), + ApiError::Internal => (StatusCode::INTERNAL_SERVER_ERROR, "internal server error").into_response(), + } + } +} + +pub fn internal_and_log(err: E) -> ApiError { // NOTE: Realize it like a macros? + error!("Internal error: {}", err); + ApiError::Internal +} + +pub fn error_and_log(err: E, error_type: ApiError) -> ApiError { + warn!("{error_type:?}: {}", err); + error_type +} \ No newline at end of file diff --git a/src/api/figura/auth.rs b/src/api/figura/auth.rs index f069aac..fbe20de 100644 --- a/src/api/figura/auth.rs +++ b/src/api/figura/auth.rs @@ -1,7 +1,7 @@ use axum::{debug_handler, extract::{Query, State}, response::{IntoResponse, Response}, routing::get, Router}; use reqwest::StatusCode; use ring::digest::{self, digest}; -use tracing::{error, info}; +use tracing::info; use crate::{auth::{has_joined, Userinfo}, utils::rand, AppState}; use super::types::auth::*; @@ -33,24 +33,33 @@ async fn verify( ) -> Response { let server_id = query.id.clone(); let username = state.user_manager.pending_remove(&server_id).unwrap().1; // TODO: Add error check - let userinfo = match has_joined(&server_id, &username).await { + let userinfo = match has_joined( + state.config.read().await.auth_providers.clone(), + &server_id, + &username + ).await { Ok(d) => d, - Err(e) => { - error!("[Authentication] {e}"); + Err(_e) => { + // error!("[Authentication] {e}"); // In auth error log already defined return (StatusCode::INTERNAL_SERVER_ERROR, "internal verify error".to_string()).into_response(); }, }; - if let Some((uuid, auth_system)) = userinfo { - info!("[Authentication] {username} logged in using {auth_system:?}"); - let authenticated = state.user_manager; - authenticated.insert( + if let Some((uuid, auth_provider)) = userinfo { + let umanager = state.user_manager; + if umanager.is_banned(&uuid) { + info!("[Authentication] {username} tried to log in, but was banned"); + return (StatusCode::BAD_REQUEST, "You're banned!".to_string()).into_response(); + } + info!("[Authentication] {username} logged in using {}", auth_provider.name); + umanager.insert( uuid, server_id.clone(), Userinfo { username, uuid, - auth_system, token: Some(server_id.clone()), + auth_provider, + ..Default::default() }, ); (StatusCode::OK, server_id.to_string()).into_response() diff --git a/src/api/figura/info.rs b/src/api/figura/info.rs index a41683b..52ff8bd 100644 --- a/src/api/figura/info.rs +++ b/src/api/figura/info.rs @@ -1,7 +1,7 @@ use axum::{extract::State, Json}; use serde_json::{json, Value}; -use crate::AppState; +use crate::{utils::get_motd, AppState}; pub async fn version() -> Json { Json(json!({ @@ -11,26 +11,26 @@ pub async fn version() -> Json { } pub async fn motd(State(state): State) -> String { - state.config.lock().await.motd.clone() + serde_json::to_string_pretty(&get_motd(state).await).unwrap() } pub async fn limits(State(state): State) -> Json { - let state = &state.config.lock().await.limitations; + let state = &state.config.read().await.limitations; Json(json!({ "rate": { - "pingSize": 1024, - "pingRate": 32, - "equip": 1, - "download": 50, - "upload": 1 + "pingSize": 1024, + "pingRate": 32, + "equip": 1, + "download": 50, + "upload": 1 }, "limits": { - "maxAvatarSize": state.max_avatar_size, - "maxAvatars": state.max_avatars, - "allowedBadges": { - "special": [0,0,0,0,0,0], - "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] - } + "maxAvatarSize": state.max_avatar_size, + "maxAvatars": state.max_avatars, + "allowedBadges": { + "special": [0,0,0,0,0,0], + "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/api/figura/profile.rs b/src/api/figura/profile.rs index 54b2944..1cf46c1 100644 --- a/src/api/figura/profile.rs +++ b/src/api/figura/profile.rs @@ -1,11 +1,9 @@ use std::sync::Arc; -use anyhow_http::{http_error_ret, response::Result}; use axum::{ - body::Bytes, extract::{Path, State}, response::IntoResponse, Json, http::header, + body::Bytes, extract::{Path, State}, Json }; use dashmap::DashMap; -use reqwest::StatusCode; use tracing::debug; use serde_json::{json, Value}; use tokio::{ @@ -15,56 +13,46 @@ use tokio::{ use uuid::Uuid; use crate::{ - auth::Token, - utils::{calculate_file_sha256, format_uuid, get_correct_array}, - AppState, + api::errors::internal_and_log, auth::Token, utils::{calculate_file_sha256, format_uuid}, ApiError, ApiResult, AppState }; use super::types::S2CMessage; pub async fn user_info( Path(uuid): Path, State(state): State, -) -> impl IntoResponse { +) -> ApiResult> { tracing::info!("Receiving profile information for {}", uuid); let formatted_uuid = format_uuid(&uuid); let avatar_file = format!("avatars/{}.moon", formatted_uuid); - let auth_system = match state.user_manager.get_by_uuid(&uuid) { - Some(d) => d.auth_system.to_string(), - None => return ( - StatusCode::BAD_REQUEST, - [(header::CONTENT_TYPE, "text/plain")], - "err".to_string() - )// (StatusCode::NO_CONTENT, "not sculptor user".to_string()), //(StatusCode::NOT_FOUND, "not found".to_string()).into_response(), + let userinfo = if let Some(info) = state.user_manager.get_by_uuid(&uuid) { info } else { + return Err(ApiError::BadRequest) // NOTE: Not Found (404) shows badge }; let mut user_info_response = json!({ "uuid": &formatted_uuid, - "rank": "default", + "rank": userinfo.rank, "equipped": [], - "lastUsed": "2024-05-11T22:20:48.884Z", + "lastUsed": userinfo.last_used, "equippedBadges": { "special": [0,0,0,0,0,0], "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] }, - "version": "0.1.4+1.20.1", - "banned": false, - "authSystem": auth_system + "version": userinfo.version, + "banned": userinfo.banned }); - if let Some(settings) = state.config.lock().await.advanced_users.get(&formatted_uuid) { - let pride = get_correct_array(settings.get("pride").unwrap()); - let special = get_correct_array(settings.get("special").unwrap()); + if let Some(settings) = state.config.read().await.advanced_users.clone().get(&uuid) { let badges = user_info_response .get_mut("equippedBadges") .and_then(Value::as_object_mut) .unwrap(); badges.append( json!({ - "special": special, - "pride": pride + "special": settings.special, + "pride": settings.pride }) .as_object_mut() .unwrap(), @@ -86,28 +74,19 @@ pub async fn user_info( } } } - ( - StatusCode::OK, - [(header::CONTENT_TYPE, "application/json")], - user_info_response.to_string() - ) + Ok(Json(user_info_response)) } -pub async fn download_avatar(Path(uuid): Path) -> Result> { +pub async fn download_avatar(Path(uuid): Path) -> ApiResult> { let uuid = format_uuid(&uuid); tracing::info!("Requesting an avatar: {}", uuid); let mut file = if let Ok(file) = fs::File::open(format!("avatars/{}.moon", uuid)).await { file } else { - http_error_ret!(NOT_FOUND, "Error! This avatar does not exist!"); + return Err(ApiError::NotFound) }; let mut buffer = Vec::new(); - file.read_to_end(&mut buffer).await?; - //match Body::from_file("avatars/74cf2ba3-f346-4dfe-b3b5-f453b9f5cc5e.moon").await { - // match Body::from_file(format!("avatars/{}.moon",uuid)).await { - // Ok(body) => Ok(Response::builder(StatusCode::Ok).body(body).build()), - // Err(e) => Err(e.into()), - // } + file.read_to_end(&mut buffer).await.map_err(|err| internal_and_log(err))?; Ok(buffer) } @@ -115,14 +94,9 @@ pub async fn upload_avatar( Token(token): Token, State(state): State, body: Bytes, -) -> Result { +) -> ApiResult { let request_data = body; - let token = match token { - Some(t) => t, - None => http_error_ret!(UNAUTHORIZED, "Authentication error!"), - }; - if let Some(user_info) = state.user_manager.get(&token) { tracing::info!( "{} ({}) trying to upload an avatar", @@ -130,24 +104,20 @@ pub async fn upload_avatar( user_info.username ); let avatar_file = format!("avatars/{}.moon", user_info.uuid); - let mut file = BufWriter::new(fs::File::create(&avatar_file).await?); - io::copy(&mut request_data.as_ref(), &mut file).await?; + let mut file = BufWriter::new(fs::File::create(&avatar_file).await.map_err(|err| internal_and_log(err))?); + io::copy(&mut request_data.as_ref(), &mut file).await.map_err(|err| internal_and_log(err))?; } Ok("ok".to_string()) } -pub async fn equip_avatar(Token(token): Token, State(state): State) -> String { +pub async fn equip_avatar(Token(token): Token, State(state): State) -> ApiResult<&'static str> { debug!("[API] S2C : Equip"); - let uuid = state.user_manager.get(&token.unwrap()).unwrap().uuid; + let uuid = state.user_manager.get(&token).ok_or_else(|| ApiError::Unauthorized)?.uuid; send_event(&state.broadcasts, &uuid); - "ok".to_string() + Ok("ok") } -pub async fn delete_avatar(Token(token): Token, State(state): State) -> Result { - let token = match token { - Some(t) => t, - None => http_error_ret!(UNAUTHORIZED, "Authentication error!"), - }; +pub async fn delete_avatar(Token(token): Token, State(state): State) -> ApiResult { if let Some(user_info) = state.user_manager.get(&token) { tracing::info!( "{} ({}) is trying to delete the avatar", @@ -155,7 +125,7 @@ pub async fn delete_avatar(Token(token): Token, State(state): State) - user_info.username ); let avatar_file = format!("avatars/{}.moon", user_info.uuid); - fs::remove_file(avatar_file).await?; + fs::remove_file(avatar_file).await.map_err(|err| internal_and_log(err))?; send_event(&state.broadcasts, &user_info.uuid); } // let avatar_file = format!("avatars/{}.moon",user_info.uuid); diff --git a/src/api/figura/websocket.rs b/src/api/figura/websocket.rs index b3a6420..2e15693 100644 --- a/src/api/figura/websocket.rs +++ b/src/api/figura/websocket.rs @@ -63,6 +63,16 @@ async fn handle_socket(mut socket: WebSocket, state: AppState) { debug!("[WebSocket{}] Receive error! Connection terminated!", owner.name()); break; }; + // Checking ban list + if let Some(ref user) = owner { + if state.user_manager.is_banned(&user.uuid) { + warn!("[WebSocket] Detected banned user with active WebSocket! Sending close with Banned code."); + let _ = socket.send(Message::Binary(S2CMessage::Toast(2, "You're banned!", None).to_vec())).await; // option слищком жирный Some("Reason: Lorum Ipsum interсно сколько влезет~~~ 0w0.") + tokio::time::sleep(std::time::Duration::from_secs(6)).await; + debug!("{:?}", socket.send(Message::Close(Some(axum::extract::ws::CloseFrame { code: 4001, reason: "You're banned!".into() }))).await); + continue; + } + } // Next is the code for processing msg let msg_vec = msg.clone().into_data(); let msg_array = msg_vec.as_slice(); @@ -70,8 +80,10 @@ async fn handle_socket(mut socket: WebSocket, state: AppState) { let newmsg = match C2SMessage::try_from(msg_array) { Ok(data) => data, Err(e) => { - error!("[WebSocket{}] This message is not from Figura! {e:?}", owner.name()); - break; + error!("[WebSocket{}] This message is not from Figura! {}", owner.name(), e.to_string()); + debug!("[WebSocket{}] Broken data: {}", owner.name(), hex::encode(msg_vec)); + continue; + // break; }, }; diff --git a/src/api/mod.rs b/src/api/mod.rs index 94107d5..a2f4786 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,2 +1,3 @@ pub mod figura; -pub mod v1; \ No newline at end of file +pub mod v1; +pub mod errors; \ No newline at end of file diff --git a/src/api/v1/avatars.rs b/src/api/v1/avatars.rs index 4d74efe..9e31212 100644 --- a/src/api/v1/avatars.rs +++ b/src/api/v1/avatars.rs @@ -1,21 +1,19 @@ -use axum::{body::Bytes, extract::{Path, State}, http::StatusCode, response::{IntoResponse, Response}}; +use axum::{body::Bytes, extract::{Path, State}}; use tokio::{fs, io::{self, BufWriter}}; +use tracing::warn; use uuid::Uuid; -use crate::{api::figura::profile::send_event, auth::Token, AppState}; +use crate::{api::figura::profile::send_event, auth::Token, ApiResult, AppState}; pub async fn upload_avatar( Path(uuid): Path, Token(token): Token, State(state): State, body: Bytes, -) -> Response { +) -> ApiResult<&'static str> { let request_data = body; - match state.config.lock().await.clone().verify_token(&token) { - Ok(_) => {}, - Err(err) => return err, - }; + state.config.read().await.clone().verify_token(&token)?; tracing::info!( "trying to upload the avatar for {}", @@ -27,18 +25,15 @@ pub async fn upload_avatar( io::copy(&mut request_data.as_ref(), &mut file).await.unwrap(); send_event(&state.broadcasts, &uuid); - (StatusCode::OK, "ok".to_string()).into_response() + Ok("ok") } pub async fn delete_avatar( Path(uuid): Path, Token(token): Token, State(state): State -) -> Response { - match state.config.lock().await.clone().verify_token(&token) { - Ok(_) => {}, - Err(err) => return err, - }; +) -> ApiResult<&'static str> { + state.config.read().await.clone().verify_token(&token)?; tracing::info!( "trying to delete the avatar for {}", @@ -48,9 +43,12 @@ pub async fn delete_avatar( let avatar_file = format!("avatars/{}.moon", &uuid); match fs::remove_file(avatar_file).await { Ok(_) => {}, - Err(_) => return (StatusCode::NOT_FOUND, "avatar doesn't exist".to_string()).into_response() + Err(_) => { + warn!("avatar doesn't exist"); + return Err(crate::ApiError::NotFound) + } }; send_event(&state.broadcasts, &uuid); - (StatusCode::OK, "ok".to_string()).into_response() + Ok("ok") } \ No newline at end of file diff --git a/src/api/v1/http2ws.rs b/src/api/v1/http2ws.rs index f11bb11..c5a1af0 100644 --- a/src/api/v1/http2ws.rs +++ b/src/api/v1/http2ws.rs @@ -1,20 +1,16 @@ -use axum::{ - extract::{Query, State}, - http::StatusCode, - response::{IntoResponse, Response} -}; -use tracing::debug; +use axum::extract::{Query, State}; +use tracing::{debug, trace, warn}; -use crate::{auth::Token, AppState}; +use crate::{api::errors::{error_and_log, internal_and_log}, auth::Token, ApiResult, AppState}; use super::types::UserUuid; pub(super) async fn verify( Token(token): Token, State(state): State, -) -> Response { - state.config.lock().await.clone() - .verify_token(&token) - .unwrap_or_else(|x| x) +) -> ApiResult<&'static str> { + state.config.read().await.clone() + .verify_token(&token)?; + Ok("ok") } pub(super) async fn raw( @@ -22,33 +18,23 @@ pub(super) async fn raw( Query(query): Query, State(state): State, body: String, -) -> Response { - debug!(body = body); - match state.config.lock().await.clone().verify_token(&token) { - Ok(_) => {}, - Err(e) => return e, - } - let payload = match hex::decode(body) { - Ok(v) => v, - Err(_) => return (StatusCode::NOT_ACCEPTABLE, "not raw data".to_string()).into_response(), - }; +) -> ApiResult<&'static str> { + trace!(body = body); + state.config.read().await.clone().verify_token(&token)?; + let payload = hex::decode(body).map_err(|err| { warn!("not raw data"); error_and_log(err, crate::ApiError::NotAcceptable) })?; debug!("{:?}", payload); match query.uuid { Some(uuid) => { // for only one - let tx = match state.session.get(&uuid) { - Some(d) => d, - None => return (StatusCode::NOT_FOUND, "unknown uuid".to_string()).into_response(), - }; - match tx.value().send(payload).await { - Ok(_) => return (StatusCode::OK, "ok".to_string()).into_response(), - Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, "cant send".to_string()).into_response(), - }; + let tx = state.session.get(&uuid).ok_or_else(|| { warn!("unknown uuid"); crate::ApiError::NotFound })?; + tx.value().send(payload).await.map_err(|err| internal_and_log(err))?; + Ok("ok") }, None => { // for all - return (StatusCode::NOT_FOUND, "uuid doesnt defined".to_string()).into_response(); + warn!("uuid doesnt defined"); + Err(crate::ApiError::NotFound) }, } } @@ -58,33 +44,22 @@ pub(super) async fn sub_raw( Query(query): Query, State(state): State, body: String, -) -> Response { - debug!(body = body); - match state.config.lock().await.clone().verify_token(&token) { - Ok(_) => {}, - Err(e) => return e, - } - let payload = match hex::decode(body) { - Ok(v) => v, - Err(_) => return (StatusCode::NOT_ACCEPTABLE, "not raw data".to_string()).into_response(), - }; +) -> ApiResult<&'static str> { + trace!(body = body); + state.config.read().await.clone().verify_token(&token)?; + let payload = hex::decode(body).map_err(|err| { warn!("not raw data"); error_and_log(err, crate::ApiError::NotAcceptable) })?; debug!("{:?}", payload); - match query.uuid { Some(uuid) => { // for only one - let tx = match state.broadcasts.get(&uuid) { - Some(d) => d, - None => return (StatusCode::NOT_FOUND, "unknown uuid".to_string()).into_response(), - }; - match tx.value().send(payload) { - Ok(_) => return (StatusCode::OK, "ok".to_string()).into_response(), - Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, "cant send".to_string()).into_response(), - }; + let tx = state.broadcasts.get(&uuid).ok_or_else(|| { warn!("unknown uuid"); crate::ApiError::NotFound })?; + tx.value().send(payload).map_err(|err| internal_and_log(err))?; + Ok("ok") }, None => { - return (StatusCode::NOT_FOUND, "uuid doesnt defined".to_string()).into_response(); + warn!("uuid doesnt defined"); + Err(crate::ApiError::NotFound) }, } } \ No newline at end of file diff --git a/src/api/v1/users.rs b/src/api/v1/users.rs index 34e8243..5ebe62b 100644 --- a/src/api/v1/users.rs +++ b/src/api/v1/users.rs @@ -1,25 +1,20 @@ use axum::{ extract::State, - http::StatusCode, - response::{IntoResponse, Response}, Json }; use tracing::debug; -use crate::{auth::{Token, Userinfo}, AppState}; +use crate::{auth::{Token, Userinfo}, ApiResult, AppState}; pub(super) async fn create_user( Token(token): Token, State(state): State, Json(json): Json -) -> Response { - match state.config.lock().await.clone().verify_token(&token) { - Ok(_) => {}, - Err(e) => return e, - } +) -> ApiResult<&'static str> { + state.config.read().await.clone().verify_token(&token)?; debug!("Creating new user: {json:?}"); state.user_manager.insert_user(json.uuid, json); - (StatusCode::OK, "ok".to_string()).into_response() + Ok("ok") } \ No newline at end of file diff --git a/src/auth/auth.rs b/src/auth/auth.rs index bce67f2..d58c0eb 100644 --- a/src/auth/auth.rs +++ b/src/auth/auth.rs @@ -1,20 +1,32 @@ -use std::sync::Arc; +use std::{sync::Arc, time::Duration}; use anyhow::anyhow; use axum::{ - async_trait, extract::{FromRequestParts, State}, http::{request::Parts, StatusCode}, response::{IntoResponse, Response} + async_trait, extract::{FromRequestParts, State}, http::{request::Parts, StatusCode} }; use dashmap::DashMap; -use tracing::{debug, trace}; +use tracing::{debug, error, trace}; use uuid::Uuid; -use crate::AppState; +use crate::{ApiError, ApiResult, AppState}; use super::types::*; +const TIMEOUT: Duration = Duration::from_secs(5); + // It's an extractor that pulls a token from the Header. #[derive(PartialEq, Debug)] -pub struct Token(pub Option); +pub struct Token(pub String); + +impl Token { + pub async fn check_auth(self, state: &AppState) -> ApiResult<()> { + if state.user_manager.is_authenticated(&self.0) { + Ok(()) + } else { + Err(ApiError::Unauthorized) + } + } +} #[async_trait] impl FromRequestParts for Token @@ -30,8 +42,8 @@ where .and_then(|value| value.to_str().ok()); trace!(token = ?token); match token { - Some(token) => Ok(Self(Some(token.to_string()))), - None => Ok(Self(None)), + Some(token) => Ok(Self(token.to_string())), + None => Err(StatusCode::UNAUTHORIZED), } } } @@ -46,53 +58,89 @@ fn get_id_json(json: &serde_json::Value) -> anyhow::Result { Ok(uuid) } -#[inline] async fn fetch_json( - auth_system: AuthSystem, + auth_provider: &AuthProvider, server_id: &str, username: &str, -) -> anyhow::Result> { - let client = reqwest::Client::new(); - let url = auth_system.get_url(); +) -> anyhow::Result> { + let client = reqwest::Client::builder().timeout(TIMEOUT).build().unwrap(); + let url = auth_provider.url.clone(); let res = client .get(url) .query(&[("serverId", server_id), ("username", username)]) .send() .await?; - debug!("{res:?}"); + trace!("{res:?}"); match res.status().as_u16() { 200 => { let json = serde_json::from_str::(&res.text().await?)?; let uuid = get_id_json(&json)?; - Ok(Some((uuid, auth_system))) + Ok(Ok((uuid, auth_provider.clone()))) } - 401 => Ok(None), // Ely.By None - 204 => Ok(None), // Mojang None - _ => Err(anyhow!("Unknown code: {}", res.status().as_u16())), + _ => Ok(Err(anyhow!("notOK: {} data: {:?}", res.status().as_u16(), res.text().await))), } } pub async fn has_joined( + AuthProviders(authproviders): AuthProviders, server_id: &str, username: &str, -) -> anyhow::Result> { - let (elyby, mojang) = ( - fetch_json(AuthSystem::ElyBy,server_id, username).await?, - fetch_json(AuthSystem::Mojang, server_id, username).await? - ); +) -> anyhow::Result> { + let (tx, mut rx) = tokio::sync::mpsc::channel(1); - if elyby.is_none() && mojang.is_none() { - Ok(None) - } else if mojang.is_some() { - Ok(mojang) - } else if elyby.is_some() { - Ok(elyby) + for provider in &authproviders { + tokio::spawn(fetch_and_send( + provider.clone(), + server_id.to_string(), + username.to_string(), + tx.clone() + )); + } + let mut errors = Vec::new(); // Counting fetches what returns errors + let mut misses = Vec::new(); // Counting non OK results + let mut prov_count: usize = authproviders.len(); + while prov_count > 0 { + if let Some(fetch_res) = rx.recv().await { + if let Ok(user_res) = fetch_res { + if let Ok(data) = user_res { + return Ok(Some(data)) + } else { + misses.push(user_res.unwrap_err()); + } + } else { + errors.push(fetch_res.unwrap_err()); + } + } else { + error!("Unexpected behavior!"); + return Err(anyhow!("Something went wrong...")) + } + prov_count -= 1; + } + + // Choosing what error return + + // Returns if some internals errors occured + if errors.len() != 0 { + error!("Something wrong with your authentification providers!\nMisses: {misses:?}\nErrors: {errors:?}"); + Err(anyhow::anyhow!("{:?}", errors)) + } else { - panic!("Impossible error!") + // Returning if user can't be authenticated + debug!("Misses: {misses:?}"); + Ok(None) } } -// End of work with external APIs + +async fn fetch_and_send( + provider: AuthProvider, + server_id: String, + username: String, + tx: tokio::sync::mpsc::Sender>> +) { + let _ = tx.send(fetch_json(&provider, &server_id, &username).await) + .await.map_err( |err| trace!("fetch_and_send error [note: ok res returned and mpsc clossed]: {err:?}")); +} // User manager #[derive(Debug, Clone)] @@ -100,7 +148,7 @@ pub struct UManager { /// Users with incomplete authentication pending: Arc>, // TODO: Add automatic purge /// Authenticated users TODO: Change name to sessions - authenticated: Arc>, // NOTE: In the future, try it in a separate LockRw branch + authenticated: Arc>, // /// Registered users registered: Arc>, } @@ -116,15 +164,24 @@ impl UManager { 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)> { + pub fn pending_remove(&self, server_id: &str) -> Option<(String, String)> { self.pending.remove(server_id) } - pub fn insert(&self, uuid: Uuid, token: String, userinfo: Userinfo) -> Option { + pub fn insert(&self, uuid: Uuid, token: String, userinfo: Userinfo) { self.authenticated.insert(token, uuid); - self.registered.insert(uuid, userinfo) + self.insert_user(uuid, userinfo); } - pub fn insert_user(&self, uuid: Uuid, userinfo: Userinfo) -> Option { - self.registered.insert(uuid, userinfo) + pub fn insert_user(&self, uuid: Uuid, userinfo: Userinfo) { + // self.registered.insert(uuid, userinfo) + let usercopy = userinfo.clone(); + self.registered.entry(uuid.clone()) + .and_modify(|exist| { + if !userinfo.username.is_empty() { exist.username = userinfo.username }; + if !userinfo.auth_provider.is_empty() { exist.auth_provider = userinfo.auth_provider }; + if userinfo.rank != Userinfo::default().rank { exist.rank = userinfo.rank }; + if userinfo.token.is_some() { exist.token = userinfo.token }; + if userinfo.version != Userinfo::default().version { exist.version = userinfo.version }; + }).or_insert(usercopy); } pub fn get( &self, @@ -139,32 +196,46 @@ impl UManager { ) -> Option> { self.registered.get(uuid) } + pub fn ban(&self, banned_user: &Userinfo) { + self.registered.entry(banned_user.uuid) + .and_modify(|exist| { + exist.banned = true; + }).or_insert(banned_user.clone()); + } + pub fn unban(&self, uuid: &Uuid) { + if let Some(mut user) = self.registered.get_mut(uuid) { + user.banned = false; + }; + } 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 is_banned(&self, uuid: &Uuid) -> bool { + if let Some(user) = self.registered.get(uuid) { user.banned } else { false } + } + pub fn count_authenticated(&self) -> usize { + self.authenticated.len() + } pub fn remove(&self, uuid: &Uuid) { - let token = self.registered.remove(uuid).unwrap().1.token.unwrap(); + let token = self.registered.get(uuid).unwrap().token.clone().unwrap(); self.authenticated.remove(&token); } } // End of User manager pub async fn check_auth( - Token(token): Token, + token: Option, State(state): State, -) -> Response { +) -> ApiResult<&'static str> { match token { Some(token) => { - if state.user_manager.is_authenticated(&token) { - (StatusCode::OK, "ok".to_string()).into_response() - } else { - (StatusCode::UNAUTHORIZED, "unauthorized".to_string()).into_response() - } + token.check_auth(&state).await?; + Ok("ok") }, - None => (StatusCode::BAD_REQUEST, "bad request".to_string()).into_response(), + None => Err(ApiError::BadRequest), } } \ No newline at end of file diff --git a/src/auth/types.rs b/src/auth/types.rs index b88a16a..7fcb0ab 100644 --- a/src/auth/types.rs +++ b/src/auth/types.rs @@ -1,56 +1,71 @@ -use std::str::FromStr; - +use chrono::Utc; use serde::Deserialize; use uuid::Uuid; -use anyhow::anyhow; #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Userinfo { - pub username: String, pub uuid: Uuid, - pub auth_system: AuthSystem, + pub username: String, + pub rank: String, + pub last_used: String, + pub auth_provider: AuthProvider, pub token: Option, + pub version: String, + pub banned: bool + +} + +impl Default for Userinfo { + fn default() -> Self { + Self { + uuid: Default::default(), + username: Default::default(), + rank: "default".to_string(), + last_used: Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true), + auth_provider: Default::default(), + token: Default::default(), + version: "0.1.4+1.20.1".to_string(), + banned: false + } + } +} + +// new part + +#[derive(Debug, Clone, PartialEq, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AuthProvider { + pub name: String, + pub url: String, +} + +impl Default for AuthProvider { + fn default() -> Self { + Self { + name: "Unknown".to_string(), + url: Default::default() + } + } +} + +impl AuthProvider { + pub fn is_empty(&self) -> bool { + if self.name == "Unknown".to_string() { + true + } else { + false + } + } } #[derive(Debug, Clone, PartialEq, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum AuthSystem { - Internal, - ElyBy, - Mojang, -} - -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 { - pub(super) 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"), - } - } -} +#[serde(rename_all = "camelCase")] +pub struct AuthProviders(pub Vec); +pub fn default_authproviders() -> AuthProviders { + AuthProviders(vec![ + AuthProvider { name: "Mojang".to_string(), url: "https://sessionserver.mojang.com/session/minecraft/hasJoined".to_string() }, + AuthProvider { name: "ElyBy".to_string(), url: "http://minecraft.ely.by/session/hasJoined".to_string() } + ]) +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index adc114e..9925c09 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,29 +1,18 @@ use anyhow::Result; use axum::{ - extract::DefaultBodyLimit, middleware::from_extractor, routing::{delete, get, post, put}, Router + extract::DefaultBodyLimit, routing::{delete, get, post, put}, Router }; use dashmap::DashMap; +use tracing_panic::panic_hook; +use tracing_subscriber::{fmt::{self, time::ChronoLocal}, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; use std::sync::Arc; -use tokio::sync::{broadcast, mpsc, Mutex}; +use tokio::{sync::{broadcast, mpsc, RwLock}, time::Instant}; use tower_http::trace::TraceLayer; use tracing::info; use uuid::Uuid; -// // WebSocket worker -// mod ws; -// use ws::handler; - -// // API: Auth -// mod auth; -// use auth::{self as api_auth, UManager}; - -// // API: Server info -// mod info; -// use info as api_info; - -// // API: Profile -// mod profile; -// use profile as api_profile; +// Errors +pub use api::errors::{ApiResult, ApiError}; // API mod api; @@ -42,14 +31,12 @@ use state::Config; // Utils mod utils; -use utils::{check_updates, update_advanced_users}; - -// // Config -// mod config; -// use config::Config; +use utils::{check_updates, get_log_file, update_advanced_users, update_bans_from_minecraft}; #[derive(Debug, Clone)] pub struct AppState { + /// Uptime + uptime: Instant, /// User manager user_manager: Arc, /// Send into WebSocket @@ -57,57 +44,91 @@ pub struct AppState { /// Ping broadcasts for WebSocket connections broadcasts: Arc>>>, /// Current configuration - config: Arc>, + config: Arc>, } const LOGGER_ENV: &'static str = "RUST_LOG"; +const CONFIG_ENV: &'static str = "RUST_CONFIG"; +const LOGS_ENV: &'static str = "LOGS_FOLDER"; const SCULPTOR_VERSION: &'static str = env!("CARGO_PKG_VERSION"); +const REPOSITORY: &'static str = "shiroyashik/sculptor"; #[tokio::main] async fn main() -> Result<()> { let _ = dotenvy::dotenv(); // "trace,axum=info,tower_http=info,tokio=info,tungstenite=info,tokio_tungstenite=info", let logger_env = std::env::var(LOGGER_ENV).unwrap_or_else(|_| "info".into()); + let config_file = std::env::var(CONFIG_ENV).unwrap_or_else(|_| "Config.toml".into()); + let logs_folder = std::env::var(LOGS_ENV).unwrap_or_else(|_| "logs".into()); - tracing_subscriber::fmt::fmt() - .with_env_filter( - logger_env - ) + let file_appender = tracing_appender::rolling::never(&logs_folder, get_log_file(&logs_folder)); + let timer = ChronoLocal::new(String::from("%Y-%m-%dT%H:%M:%S%.3f%:z")); + + let file_layer = fmt::layer() + .with_ansi(false) // Disable ANSI colors for file logs + .with_timer(timer.clone()) .pretty() + .with_writer(file_appender); + + // Create a layer for the terminal + let terminal_layer = fmt::layer() + .with_ansi(true) + .with_timer(timer) + .pretty() + .with_writer(std::io::stdout); + + // Combine the layers and set the global subscriber + tracing_subscriber::registry() + .with(EnvFilter::from(logger_env)) + .with(file_layer) + .with(terminal_layer) .init(); - info!("The Sculptor v{}{}", SCULPTOR_VERSION, check_updates("shiroyashik/sculptor", &SCULPTOR_VERSION).await?); - - let config_file = std::env::var("CONFIG_PATH").unwrap_or_else(|_| "Config.toml".into()); + std::panic::set_hook(Box::new(panic_hook)); + // let prev_hook = std::panic::take_hook(); + // std::panic::set_hook(Box::new(move |panic_info| { + // panic_hook(panic_info); + // prev_hook(panic_info); + // })); + + info!("The Sculptor v{}{}", SCULPTOR_VERSION, check_updates(REPOSITORY, &SCULPTOR_VERSION).await?); + // Config - let config = Arc::new(Mutex::new(Config::parse(config_file.clone().into()))); - let listen = config.lock().await.listen.clone(); + let config = Arc::new(RwLock::new(Config::parse(config_file.clone().into()))); + let listen = config.read().await.listen.clone(); // State let state = AppState { + uptime: Instant::now(), user_manager: Arc::new(UManager::new()), session: Arc::new(DashMap::new()), broadcasts: Arc::new(DashMap::new()), - config: config, + config, }; // Automatic update of configuration while the server is running let config_update = Arc::clone(&state.config); let user_manager = Arc::clone(&state.user_manager); - update_advanced_users(&config_update.lock().await.advanced_users, &user_manager); + update_advanced_users(&config_update.read().await.advanced_users.clone(), &user_manager); tokio::spawn(async move { loop { tokio::time::sleep(std::time::Duration::from_secs(10)).await; let new_config = Config::parse(config_file.clone().into()); - let mut config = config_update.lock().await; + let mut config = config_update.write().await; if new_config != *config { info!("Server configuration modification detected!"); *config = new_config; - update_advanced_users(&config.advanced_users, &user_manager); + update_advanced_users(&config.advanced_users.clone(), &user_manager); } } }); + if state.config.read().await.mc_folder.exists() { + tokio::spawn(update_bans_from_minecraft( + state.config.read().await.mc_folder.clone(), + Arc::clone(&state.user_manager) + )); + } let api = Router::new() .nest("//auth", api_auth::router()) @@ -126,7 +147,6 @@ async fn main() -> Result<()> { .nest("/api", api) .route("/ws", get(ws)) .route("/health", get(|| async { "ok" })) - .route_layer(from_extractor::()) .with_state(state) .layer(TraceLayer::new_for_http().on_request(())); @@ -155,8 +175,13 @@ async fn shutdown_signal() { #[cfg(not(unix))] let terminate = std::future::pending::<()>(); tokio::select! { - () = ctrl_c => {}, - () = terminate => {}, + () = ctrl_c => { + println!(); + info!("Ctrl+C signal received"); + }, + () = terminate => { + println!(); + info!("Terminate signal received"); + }, } - info!("Terminate signal received"); } diff --git a/src/state/config.rs b/src/state/config.rs index a851277..0f73880 100644 --- a/src/state/config.rs +++ b/src/state/config.rs @@ -1,37 +1,37 @@ -use std::{io::Read, path::PathBuf}; +use std::{collections::HashMap, io::Read, path::PathBuf}; use serde::Deserialize; -use toml::Table; +use tracing::{debug, warn}; +use uuid::Uuid; + +use crate::auth::{default_authproviders, AuthProviders, Userinfo}; #[derive(Deserialize, Clone, Debug, PartialEq)] #[serde(rename_all = "camelCase")] pub struct Config { pub listen: String, pub token: Option, - pub motd: String, + pub motd: CMotd, + #[serde(default = "default_authproviders")] + pub auth_providers: AuthProviders, pub limitations: Limitations, - pub advanced_users: Table, + #[serde(default)] + pub mc_folder: PathBuf, + #[serde(default)] + pub advanced_users: HashMap, } -impl Config { - pub fn verify_token(&self, suspicious: &Option) -> Result { - use axum::{http::StatusCode, response::IntoResponse}; - match &self.token { - Some(token) => { - match suspicious { - Some(suspicious) => { - if token == suspicious { - return Ok((StatusCode::OK, "ok".to_string()).into_response()) - } else { - return Err((StatusCode::UNAUTHORIZED, "wrong token".to_string()).into_response()) - } - }, - None => return Err((StatusCode::UNAUTHORIZED, "unauthorized".to_string()).into_response()) - } - }, - None => return Err((StatusCode::LOCKED, "token doesnt defined".to_string()).into_response()), - } - } +#[derive(Deserialize, Clone, Debug, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct CMotd { + pub display_server_info: bool, + pub custom_text: String, + #[serde(rename = "sInfoUptime")] + pub text_uptime: String, + #[serde(rename = "sInfoAuthClients")] + pub text_authclients: String, + #[serde(rename = "sInfoDrawIndent")] + pub draw_indent: bool, } #[derive(Deserialize, Clone, Debug, PartialEq)] @@ -41,6 +41,37 @@ pub struct Limitations { pub max_avatars: u64, } +#[derive(Deserialize, Clone, Debug, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct AdvancedUsers { + #[serde(default)] + pub username: String, + #[serde(default)] + pub banned: bool, + #[serde(default)] + pub special: [u8;6], + #[serde(default)] + pub pride: [u8;25], +} + +#[derive(Deserialize, Clone, Debug, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct BannedPlayer { + pub uuid: Uuid, + pub name: String, +} + +impl Into for BannedPlayer { + fn into(self) -> Userinfo { + Userinfo { + uuid: self.uuid, + username: self.name, + banned: true, + ..Default::default() + } + } +} + impl Config { pub fn parse(path: PathBuf) -> Self { let mut file = std::fs::File::open(path).expect("Access denied or file doesn't exists!"); @@ -49,4 +80,23 @@ impl Config { toml::from_str(&data).unwrap() } -} + + pub fn verify_token(&self, suspicious: &str) -> crate::ApiResult<()> { + use crate::ApiError; + match &self.token { + Some(token) => { + if token == suspicious { + debug!("Admin token passed!"); + Ok(()) + } else { + warn!("Unknown tryed to use admin functions, but use wrong token!"); + Err(ApiError::Unauthorized) + } + }, + None => { + warn!("Unknown tryed to use admin functions, but token is not defined!"); + Err(ApiError::BadRequest) + }, + } + } +} \ No newline at end of file diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 752ad4c..05f72f6 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,5 +1,7 @@ mod utils; mod check_updates; +mod motd; pub use utils::*; +pub use motd::*; pub use check_updates::check_updates; \ No newline at end of file diff --git a/src/utils/motd.rs b/src/utils/motd.rs new file mode 100644 index 0000000..5b76d34 --- /dev/null +++ b/src/utils/motd.rs @@ -0,0 +1,69 @@ +use chrono::Duration; +use serde::{Deserialize, Serialize}; +use tracing::error; + +use crate::AppState; + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Motd { + pub text: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub color: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub click_event: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub underlined: Option, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ClickEvent { + pub action: String, + pub value: String, +} + +pub async fn get_motd(state: AppState) -> Vec { + let motd_settings = &state.config.read().await.motd; + + let custom: Result, serde_json::Error> = serde_json::from_str(&motd_settings.custom_text).map_err(|e| { error!("Can't parse custom MOTD!\n{e:?}"); e}); + if !motd_settings.display_server_info { + return custom.unwrap(); + } + + // let time = Local::now().format("%H:%M"); + let uptime = state.uptime.elapsed().as_secs(); + let duration = Duration::seconds(uptime.try_into().unwrap()); + let hours = duration.num_hours(); + let minutes = duration.num_minutes() % 60; + let seconds = duration.num_seconds() % 60; + + let mut ser_info = vec![ + // Motd { + // text: format!("Generated at {time}\n"), + // ..Default::default() + // }, + Motd { + text: format!("{}{:02}:{:02}:{:02}\n", motd_settings.text_uptime, hours, minutes, seconds), + ..Default::default() + }, + Motd { + text: format!("{}{}\n", motd_settings.text_authclients, state.user_manager.count_authenticated()), + ..Default::default() + }, + ]; + + if motd_settings.draw_indent { + ser_info.push(Motd { + text: "----\n\n".to_string(), + color: Some("gold".to_string()), + ..Default::default() + }) + } + + if let Ok(custom) = custom { + [ser_info, custom].concat() + } else { + ser_info + } +} \ No newline at end of file diff --git a/src/utils/utils.rs b/src/utils/utils.rs index 0db2562..92a83e6 100644 --- a/src/utils/utils.rs +++ b/src/utils/utils.rs @@ -1,11 +1,14 @@ -use std::{fs::File, io::Read, str::FromStr}; +use std::{fs::File, io::Read, path::{Path, PathBuf}}; use base64::prelude::*; use rand::{distributions::Alphanumeric, thread_rng, Rng}; use ring::digest::{self, digest}; +use tokio::io::AsyncReadExt; +use tracing::{error, info}; use uuid::Uuid; +use chrono::prelude::*; -use crate::auth::{AuthSystem, UManager, Userinfo}; +use crate::{auth::{UManager, Userinfo}, state::{AdvancedUsers, BannedPlayer}}; // Core functions pub fn rand() -> [u8; 50] { @@ -27,50 +30,79 @@ pub fn _generate_hex_string(length: usize) -> String { hex::encode(random_bytes) } -pub fn get_correct_array(value: &toml::Value) -> Vec { - // let res: Vec; - value - .as_array() - .unwrap() - .iter() - .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 update_advanced_users(value: &toml::Table, umanager: &UManager) { +pub fn update_advanced_users(value: &std::collections::HashMap, umanager: &UManager) { let users: 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 + uuid.clone(), + Userinfo { + uuid: uuid.clone(), + username: userdata.username.clone(), + banned: userdata.banned, + ..Default::default() } )}) .collect(); for (uuid, userinfo) in users { - umanager.insert_user(uuid, userinfo); + umanager.insert_user(uuid, userinfo.clone()); + if userinfo.banned { + umanager.ban(&userinfo) + } + } +} + +pub async fn update_bans_from_minecraft(folder: PathBuf, umanager: std::sync::Arc) { + let path = folder.join("banned-players.json"); + let mut file = tokio::fs::File::open(path.clone()).await.expect("Access denied or banned-players.json doesn't exists!"); + let mut data = String::new(); + // vars end + + // initialize + file.read_to_string(&mut data).await.expect("cant read banned-players.json"); + let mut old_bans: Vec = serde_json::from_str(&data).expect("cant parse banned-players.json"); + + if !old_bans.is_empty() { + let names: Vec = old_bans.iter().map(|user| user.name.clone()).collect(); + info!("Banned players: {}", names.join(", ")); + } + + for player in &old_bans { + umanager.ban(&player.clone().into()); + } + + // old_bans + loop { + tokio::time::sleep(std::time::Duration::from_secs(10)).await; + let mut file = tokio::fs::File::open(path.clone()).await.expect("Access denied or file doesn't exists!"); + let mut data = String::new(); + file.read_to_string(&mut data).await.expect("cant read banned-players.json"); + let new_bans: Vec = if let Ok(res) = serde_json::from_str(&data) { res } else { + error!("Error occured while parsing a banned-players.json"); + continue; + }; + + if new_bans != old_bans { + info!("Minecraft ban list modification detected!"); + let unban: Vec<&BannedPlayer> = old_bans.iter().filter(|user| !new_bans.contains(user)).collect(); + let mut unban_names = unban.iter().map(|user| user.name.clone()).collect::>().join(", "); + if !unban.is_empty() { + for player in unban { + umanager.unban(&player.uuid); + } + } else { unban_names = String::from("-")}; + let ban: Vec<&BannedPlayer> = new_bans.iter().filter(|user| !old_bans.contains(user)).collect(); + let mut ban_names = ban.iter().map(|user| user.name.clone()).collect::>().join(", "); + if !ban.is_empty() { + for player in ban { + umanager.ban(&player.clone().into()); + } + } else { ban_names = String::from("-")}; + info!("List of changes:\n Banned: {ban_names}\n Unbanned: {unban_names}"); + // Write new to old for next iteration + old_bans = new_bans; + } } } @@ -99,3 +131,16 @@ pub fn calculate_file_sha256(file_path: &str) -> Result Ok(hex_hash) } + +pub fn get_log_file(folder: &str) -> String { + let local_date = Local::now().format("%Y-%m-%d"); + let mut index: u16 = 0; + loop { + let file_name = format!("{local_date}.{:04}.log", index); + let file_path = Path::new(folder).join(&file_name); + if !Path::new(&file_path).exists() { + return file_name; + } + index += 1; + } +} \ No newline at end of file