+ log files

+ more control on auth providers
+ server info in motd
+ bans and parsing minecraft server blacklist
+ more error handling
+ panic hook to tracing
This commit is contained in:
Shiroyasha 2024-08-15 14:10:39 +03:00
parent bd101fc3fa
commit d45a495cbf
Signed by: shiroyashik
GPG key ID: E4953D3940D7860A
21 changed files with 748 additions and 378 deletions

3
.gitignore vendored
View file

@ -1,7 +1,10 @@
/target /target
/Assets-main /Assets-main
/avatars /avatars
/logs
output.log output.log
docker-compose.yml docker-compose.yml
Config.toml Config.toml
.env .env
perf.data*
banned-players.json

108
Cargo.lock generated
View file

@ -47,21 +47,6 @@ version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" 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]] [[package]]
name = "async-trait" name = "async-trait"
version = "0.1.81" version = "0.1.81"
@ -380,6 +365,15 @@ dependencies = [
"winapi", "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]] [[package]]
name = "crossbeam-utils" name = "crossbeam-utils"
version = "0.8.20" version = "0.8.20"
@ -426,6 +420,15 @@ version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2"
[[package]]
name = "deranged"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
dependencies = [
"powerfmt",
]
[[package]] [[package]]
name = "digest" name = "digest"
version = "0.10.7" version = "0.10.7"
@ -1028,6 +1031,12 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "num-conv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]] [[package]]
name = "num-traits" name = "num-traits"
version = "0.2.19" version = "0.2.19"
@ -1184,6 +1193,12 @@ version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]] [[package]]
name = "ppv-lite86" name = "ppv-lite86"
version = "0.2.17" version = "0.2.17"
@ -1481,10 +1496,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]] [[package]]
name = "sculptor" name = "sculptor"
version = "0.2.3" version = "0.3.0-dev"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"anyhow-http",
"axum", "axum",
"base64 0.22.1", "base64 0.22.1",
"chrono", "chrono",
@ -1498,10 +1512,13 @@ dependencies = [
"semver", "semver",
"serde", "serde",
"serde_json", "serde_json",
"thiserror",
"tokio", "tokio",
"toml 0.8.16", "toml 0.8.16",
"tower-http", "tower-http",
"tracing", "tracing",
"tracing-appender",
"tracing-panic",
"tracing-subscriber", "tracing-subscriber",
"uuid", "uuid",
] ]
@ -1812,6 +1829,37 @@ dependencies = [
"once_cell", "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]] [[package]]
name = "tinyvec" name = "tinyvec"
version = "1.8.0" version = "1.8.0"
@ -1829,9 +1877,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.39.1" version = "1.39.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d040ac2b29ab03b09d4129c2f5bbd012a3ac2f79d38ff506a4bf8dd34b0eac8a" checksum = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1"
dependencies = [ dependencies = [
"backtrace", "backtrace",
"bytes", "bytes",
@ -2002,6 +2050,18 @@ dependencies = [
"tracing-core", "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]] [[package]]
name = "tracing-attributes" name = "tracing-attributes"
version = "0.1.27" version = "0.1.27"
@ -2034,6 +2094,16 @@ dependencies = [
"tracing-core", "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]] [[package]]
name = "tracing-subscriber" name = "tracing-subscriber"
version = "0.3.18" version = "0.3.18"

View file

@ -1,20 +1,20 @@
[package] [package]
name = "sculptor" name = "sculptor"
authors = ["Shiroyashik <shiroyashik@shsr.ru>"] authors = ["Shiroyashik <shiroyashik@shsr.ru>"]
version = "0.2.3" version = "0.3.0-dev"
edition = "2021" edition = "2021"
publish = false publish = false
[dependencies] [dependencies]
# Logging # Logging
tracing-subscriber = { version = "0.3.18", features = ["env-filter", "chrono"] } tracing-subscriber = { version = "0.3.18", features = ["env-filter", "chrono"] }
tracing-appender = "0.2.3"
tracing-panic = "0.1.2"
tracing = "0.1.40" tracing = "0.1.40"
# Errors handelers # Errors handelers
anyhow = "1.0.83" anyhow = "1.0.83"
anyhow-http = { version = "0.3.0", features = ["axum"] } thiserror = "1.0.63"
# Serialization
chrono = { version = "0.4.38", features = ["now", "serde"] } chrono = { version = "0.4.38", features = ["now", "serde"] }
serde = { version = "1.0.201", features = ["derive"] } serde = { version = "1.0.201", features = ["derive"] }
serde_json = "1.0.117" serde_json = "1.0.117"

View file

@ -4,9 +4,25 @@ listen = "0.0.0.0:6665"
## Don't touch if you don't know what you're doing ## Don't touch if you don't know what you're doing
# token = "<random symbols>" # token = "<random symbols>"
## 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 ## Message of The Day
## It will be displayed to every player in the Figura menu who is connected to your server ## 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 " "text": "You are connected to "
@ -48,18 +64,15 @@ motd = """
maxAvatarSize = 100000 # 100 KB maxAvatarSize = 100000 # 100 KB
maxAvatars = 10 maxAvatars = 10
[advancedUsers] [advancedUsers.66004548-4de5-49de-bade-9c3933d8eb97]
username = "Shiroyashik"
# [advancedUsers.66004548-4de5-49de-bade-9c3933d8eb97] special = [0,0,0,1,0,0] # 6
# username = "Shiroyashik" 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
# 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
## With advancedUsers you can set additional parameters ## With advancedUsers you can set additional parameters
# [advancedUsers.your uuid here] # [advancedUsers.your uuid here]
# username = "Your_username_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 # 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] # 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]

View file

@ -53,3 +53,9 @@ Pride badges
23 Полисексуал 23 Полисексуал
24 Прайд 24 Прайд
25 Трансгендер 25 Трансгендер
Toast
0 Blue (Default)
1 Yellow (Warning)
2 Red (Error)
3 Cookie! (OwO)

41
src/api/errors.rs Normal file
View file

@ -0,0 +1,41 @@
use axum::{http::StatusCode, response::{IntoResponse, Response}};
use thiserror::Error;
use tracing::{error, warn};
pub type ApiResult<T> = Result<T, ApiError>;
#[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<E: std::fmt::Display>(err: E) -> ApiError { // NOTE: Realize it like a macros?
error!("Internal error: {}", err);
ApiError::Internal
}
pub fn error_and_log<E: std::fmt::Display>(err: E, error_type: ApiError) -> ApiError {
warn!("{error_type:?}: {}", err);
error_type
}

View file

@ -1,7 +1,7 @@
use axum::{debug_handler, extract::{Query, State}, response::{IntoResponse, Response}, routing::get, Router}; use axum::{debug_handler, extract::{Query, State}, response::{IntoResponse, Response}, routing::get, Router};
use reqwest::StatusCode; use reqwest::StatusCode;
use ring::digest::{self, digest}; use ring::digest::{self, digest};
use tracing::{error, info}; use tracing::info;
use crate::{auth::{has_joined, Userinfo}, utils::rand, AppState}; use crate::{auth::{has_joined, Userinfo}, utils::rand, AppState};
use super::types::auth::*; use super::types::auth::*;
@ -33,24 +33,33 @@ async fn verify(
) -> Response { ) -> Response {
let server_id = query.id.clone(); let server_id = query.id.clone();
let username = state.user_manager.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 { let userinfo = match has_joined(
state.config.read().await.auth_providers.clone(),
&server_id,
&username
).await {
Ok(d) => d, Ok(d) => d,
Err(e) => { Err(_e) => {
error!("[Authentication] {e}"); // error!("[Authentication] {e}"); // In auth error log already defined
return (StatusCode::INTERNAL_SERVER_ERROR, "internal verify error".to_string()).into_response(); return (StatusCode::INTERNAL_SERVER_ERROR, "internal verify error".to_string()).into_response();
}, },
}; };
if let Some((uuid, auth_system)) = userinfo { if let Some((uuid, auth_provider)) = userinfo {
info!("[Authentication] {username} logged in using {auth_system:?}"); let umanager = state.user_manager;
let authenticated = state.user_manager; if umanager.is_banned(&uuid) {
authenticated.insert( 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, uuid,
server_id.clone(), server_id.clone(),
Userinfo { Userinfo {
username, username,
uuid, uuid,
auth_system,
token: Some(server_id.clone()), token: Some(server_id.clone()),
auth_provider,
..Default::default()
}, },
); );
(StatusCode::OK, server_id.to_string()).into_response() (StatusCode::OK, server_id.to_string()).into_response()

View file

@ -1,7 +1,7 @@
use axum::{extract::State, Json}; use axum::{extract::State, Json};
use serde_json::{json, Value}; use serde_json::{json, Value};
use crate::AppState; use crate::{utils::get_motd, AppState};
pub async fn version() -> Json<Value> { pub async fn version() -> Json<Value> {
Json(json!({ Json(json!({
@ -11,26 +11,26 @@ pub async fn version() -> Json<Value> {
} }
pub async fn motd(State(state): State<AppState>) -> String { pub async fn motd(State(state): State<AppState>) -> String {
state.config.lock().await.motd.clone() serde_json::to_string_pretty(&get_motd(state).await).unwrap()
} }
pub async fn limits(State(state): State<AppState>) -> Json<Value> { pub async fn limits(State(state): State<AppState>) -> Json<Value> {
let state = &state.config.lock().await.limitations; let state = &state.config.read().await.limitations;
Json(json!({ Json(json!({
"rate": { "rate": {
"pingSize": 1024, "pingSize": 1024,
"pingRate": 32, "pingRate": 32,
"equip": 1, "equip": 1,
"download": 50, "download": 50,
"upload": 1 "upload": 1
}, },
"limits": { "limits": {
"maxAvatarSize": state.max_avatar_size, "maxAvatarSize": state.max_avatar_size,
"maxAvatars": state.max_avatars, "maxAvatars": state.max_avatars,
"allowedBadges": { "allowedBadges": {
"special": [0,0,0,0,0,0], "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] "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]
} }
} }
})) }))
} }

View file

@ -1,11 +1,9 @@
use std::sync::Arc; use std::sync::Arc;
use anyhow_http::{http_error_ret, response::Result};
use axum::{ use axum::{
body::Bytes, extract::{Path, State}, response::IntoResponse, Json, http::header, body::Bytes, extract::{Path, State}, Json
}; };
use dashmap::DashMap; use dashmap::DashMap;
use reqwest::StatusCode;
use tracing::debug; use tracing::debug;
use serde_json::{json, Value}; use serde_json::{json, Value};
use tokio::{ use tokio::{
@ -15,56 +13,46 @@ use tokio::{
use uuid::Uuid; use uuid::Uuid;
use crate::{ use crate::{
auth::Token, api::errors::internal_and_log, auth::Token, utils::{calculate_file_sha256, format_uuid}, ApiError, ApiResult, AppState
utils::{calculate_file_sha256, format_uuid, get_correct_array},
AppState,
}; };
use super::types::S2CMessage; use super::types::S2CMessage;
pub async fn user_info( pub async fn user_info(
Path(uuid): Path<Uuid>, Path(uuid): Path<Uuid>,
State(state): State<AppState>, State(state): State<AppState>,
) -> impl IntoResponse { ) -> ApiResult<Json<Value>> {
tracing::info!("Receiving profile information for {}", uuid); tracing::info!("Receiving profile information for {}", uuid);
let formatted_uuid = format_uuid(&uuid); let formatted_uuid = format_uuid(&uuid);
let avatar_file = format!("avatars/{}.moon", formatted_uuid); let avatar_file = format!("avatars/{}.moon", formatted_uuid);
let auth_system = match state.user_manager.get_by_uuid(&uuid) { let userinfo = if let Some(info) = state.user_manager.get_by_uuid(&uuid) { info } else {
Some(d) => d.auth_system.to_string(), return Err(ApiError::BadRequest) // NOTE: Not Found (404) shows badge
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 mut user_info_response = json!({ let mut user_info_response = json!({
"uuid": &formatted_uuid, "uuid": &formatted_uuid,
"rank": "default", "rank": userinfo.rank,
"equipped": [], "equipped": [],
"lastUsed": "2024-05-11T22:20:48.884Z", "lastUsed": userinfo.last_used,
"equippedBadges": { "equippedBadges": {
"special": [0,0,0,0,0,0], "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] "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", "version": userinfo.version,
"banned": false, "banned": userinfo.banned
"authSystem": auth_system
}); });
if let Some(settings) = state.config.lock().await.advanced_users.get(&formatted_uuid) { if let Some(settings) = state.config.read().await.advanced_users.clone().get(&uuid) {
let pride = get_correct_array(settings.get("pride").unwrap());
let special = get_correct_array(settings.get("special").unwrap());
let badges = user_info_response let badges = user_info_response
.get_mut("equippedBadges") .get_mut("equippedBadges")
.and_then(Value::as_object_mut) .and_then(Value::as_object_mut)
.unwrap(); .unwrap();
badges.append( badges.append(
json!({ json!({
"special": special, "special": settings.special,
"pride": pride "pride": settings.pride
}) })
.as_object_mut() .as_object_mut()
.unwrap(), .unwrap(),
@ -86,28 +74,19 @@ pub async fn user_info(
} }
} }
} }
( Ok(Json(user_info_response))
StatusCode::OK,
[(header::CONTENT_TYPE, "application/json")],
user_info_response.to_string()
)
} }
pub async fn download_avatar(Path(uuid): Path<Uuid>) -> Result<Vec<u8>> { pub async fn download_avatar(Path(uuid): Path<Uuid>) -> ApiResult<Vec<u8>> {
let uuid = format_uuid(&uuid); let uuid = format_uuid(&uuid);
tracing::info!("Requesting an avatar: {}", uuid); tracing::info!("Requesting an avatar: {}", uuid);
let mut file = if let Ok(file) = fs::File::open(format!("avatars/{}.moon", uuid)).await { let mut file = if let Ok(file) = fs::File::open(format!("avatars/{}.moon", uuid)).await {
file file
} else { } else {
http_error_ret!(NOT_FOUND, "Error! This avatar does not exist!"); return Err(ApiError::NotFound)
}; };
let mut buffer = Vec::new(); let mut buffer = Vec::new();
file.read_to_end(&mut buffer).await?; file.read_to_end(&mut buffer).await.map_err(|err| internal_and_log(err))?;
//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()),
// }
Ok(buffer) Ok(buffer)
} }
@ -115,14 +94,9 @@ pub async fn upload_avatar(
Token(token): Token, Token(token): Token,
State(state): State<AppState>, State(state): State<AppState>,
body: Bytes, body: Bytes,
) -> Result<String> { ) -> ApiResult<String> {
let request_data = body; 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) { if let Some(user_info) = state.user_manager.get(&token) {
tracing::info!( tracing::info!(
"{} ({}) trying to upload an avatar", "{} ({}) trying to upload an avatar",
@ -130,24 +104,20 @@ pub async fn upload_avatar(
user_info.username user_info.username
); );
let avatar_file = format!("avatars/{}.moon", user_info.uuid); let avatar_file = format!("avatars/{}.moon", user_info.uuid);
let mut file = BufWriter::new(fs::File::create(&avatar_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?; io::copy(&mut request_data.as_ref(), &mut file).await.map_err(|err| internal_and_log(err))?;
} }
Ok("ok".to_string()) Ok("ok".to_string())
} }
pub async fn equip_avatar(Token(token): Token, State(state): State<AppState>) -> String { pub async fn equip_avatar(Token(token): Token, State(state): State<AppState>) -> ApiResult<&'static str> {
debug!("[API] S2C : Equip"); 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); send_event(&state.broadcasts, &uuid);
"ok".to_string() Ok("ok")
} }
pub async fn delete_avatar(Token(token): Token, State(state): State<AppState>) -> Result<String> { pub async fn delete_avatar(Token(token): Token, State(state): State<AppState>) -> ApiResult<String> {
let token = match token {
Some(t) => t,
None => http_error_ret!(UNAUTHORIZED, "Authentication error!"),
};
if let Some(user_info) = state.user_manager.get(&token) { if let Some(user_info) = state.user_manager.get(&token) {
tracing::info!( tracing::info!(
"{} ({}) is trying to delete the avatar", "{} ({}) is trying to delete the avatar",
@ -155,7 +125,7 @@ pub async fn delete_avatar(Token(token): Token, State(state): State<AppState>) -
user_info.username user_info.username
); );
let avatar_file = format!("avatars/{}.moon", user_info.uuid); 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); send_event(&state.broadcasts, &user_info.uuid);
} }
// let avatar_file = format!("avatars/{}.moon",user_info.uuid); // let avatar_file = format!("avatars/{}.moon",user_info.uuid);

View file

@ -63,6 +63,16 @@ async fn handle_socket(mut socket: WebSocket, state: AppState) {
debug!("[WebSocket{}] Receive error! Connection terminated!", owner.name()); debug!("[WebSocket{}] Receive error! Connection terminated!", owner.name());
break; 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 // Next is the code for processing msg
let msg_vec = msg.clone().into_data(); let msg_vec = msg.clone().into_data();
let msg_array = msg_vec.as_slice(); 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) { let newmsg = match C2SMessage::try_from(msg_array) {
Ok(data) => data, Ok(data) => data,
Err(e) => { Err(e) => {
error!("[WebSocket{}] This message is not from Figura! {e:?}", owner.name()); error!("[WebSocket{}] This message is not from Figura! {}", owner.name(), e.to_string());
break; debug!("[WebSocket{}] Broken data: {}", owner.name(), hex::encode(msg_vec));
continue;
// break;
}, },
}; };

View file

@ -1,2 +1,3 @@
pub mod figura; pub mod figura;
pub mod v1; pub mod v1;
pub mod errors;

View file

@ -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 tokio::{fs, io::{self, BufWriter}};
use tracing::warn;
use uuid::Uuid; 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( pub async fn upload_avatar(
Path(uuid): Path<Uuid>, Path(uuid): Path<Uuid>,
Token(token): Token, Token(token): Token,
State(state): State<AppState>, State(state): State<AppState>,
body: Bytes, body: Bytes,
) -> Response { ) -> ApiResult<&'static str> {
let request_data = body; let request_data = body;
match state.config.lock().await.clone().verify_token(&token) { state.config.read().await.clone().verify_token(&token)?;
Ok(_) => {},
Err(err) => return err,
};
tracing::info!( tracing::info!(
"trying to upload the avatar for {}", "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(); io::copy(&mut request_data.as_ref(), &mut file).await.unwrap();
send_event(&state.broadcasts, &uuid); send_event(&state.broadcasts, &uuid);
(StatusCode::OK, "ok".to_string()).into_response() Ok("ok")
} }
pub async fn delete_avatar( pub async fn delete_avatar(
Path(uuid): Path<Uuid>, Path(uuid): Path<Uuid>,
Token(token): Token, Token(token): Token,
State(state): State<AppState> State(state): State<AppState>
) -> Response { ) -> ApiResult<&'static str> {
match state.config.lock().await.clone().verify_token(&token) { state.config.read().await.clone().verify_token(&token)?;
Ok(_) => {},
Err(err) => return err,
};
tracing::info!( tracing::info!(
"trying to delete the avatar for {}", "trying to delete the avatar for {}",
@ -48,9 +43,12 @@ pub async fn delete_avatar(
let avatar_file = format!("avatars/{}.moon", &uuid); let avatar_file = format!("avatars/{}.moon", &uuid);
match fs::remove_file(avatar_file).await { match fs::remove_file(avatar_file).await {
Ok(_) => {}, 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); send_event(&state.broadcasts, &uuid);
(StatusCode::OK, "ok".to_string()).into_response() Ok("ok")
} }

View file

@ -1,20 +1,16 @@
use axum::{ use axum::extract::{Query, State};
extract::{Query, State}, use tracing::{debug, trace, warn};
http::StatusCode,
response::{IntoResponse, Response}
};
use tracing::debug;
use crate::{auth::Token, AppState}; use crate::{api::errors::{error_and_log, internal_and_log}, auth::Token, ApiResult, AppState};
use super::types::UserUuid; use super::types::UserUuid;
pub(super) async fn verify( pub(super) async fn verify(
Token(token): Token, Token(token): Token,
State(state): State<AppState>, State(state): State<AppState>,
) -> Response { ) -> ApiResult<&'static str> {
state.config.lock().await.clone() state.config.read().await.clone()
.verify_token(&token) .verify_token(&token)?;
.unwrap_or_else(|x| x) Ok("ok")
} }
pub(super) async fn raw( pub(super) async fn raw(
@ -22,33 +18,23 @@ pub(super) async fn raw(
Query(query): Query<UserUuid>, Query(query): Query<UserUuid>,
State(state): State<AppState>, State(state): State<AppState>,
body: String, body: String,
) -> Response { ) -> ApiResult<&'static str> {
debug!(body = body); trace!(body = body);
match state.config.lock().await.clone().verify_token(&token) { state.config.read().await.clone().verify_token(&token)?;
Ok(_) => {}, let payload = hex::decode(body).map_err(|err| { warn!("not raw data"); error_and_log(err, crate::ApiError::NotAcceptable) })?;
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(),
};
debug!("{:?}", payload); debug!("{:?}", payload);
match query.uuid { match query.uuid {
Some(uuid) => { Some(uuid) => {
// for only one // for only one
let tx = match state.session.get(&uuid) { let tx = state.session.get(&uuid).ok_or_else(|| { warn!("unknown uuid"); crate::ApiError::NotFound })?;
Some(d) => d, tx.value().send(payload).await.map_err(|err| internal_and_log(err))?;
None => return (StatusCode::NOT_FOUND, "unknown uuid".to_string()).into_response(), Ok("ok")
};
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(),
};
}, },
None => { None => {
// for all // 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<UserUuid>, Query(query): Query<UserUuid>,
State(state): State<AppState>, State(state): State<AppState>,
body: String, body: String,
) -> Response { ) -> ApiResult<&'static str> {
debug!(body = body); trace!(body = body);
match state.config.lock().await.clone().verify_token(&token) { state.config.read().await.clone().verify_token(&token)?;
Ok(_) => {}, let payload = hex::decode(body).map_err(|err| { warn!("not raw data"); error_and_log(err, crate::ApiError::NotAcceptable) })?;
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(),
};
debug!("{:?}", payload); debug!("{:?}", payload);
match query.uuid { match query.uuid {
Some(uuid) => { Some(uuid) => {
// for only one // for only one
let tx = match state.broadcasts.get(&uuid) { let tx = state.broadcasts.get(&uuid).ok_or_else(|| { warn!("unknown uuid"); crate::ApiError::NotFound })?;
Some(d) => d, tx.value().send(payload).map_err(|err| internal_and_log(err))?;
None => return (StatusCode::NOT_FOUND, "unknown uuid".to_string()).into_response(), Ok("ok")
};
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(),
};
}, },
None => { None => {
return (StatusCode::NOT_FOUND, "uuid doesnt defined".to_string()).into_response(); warn!("uuid doesnt defined");
Err(crate::ApiError::NotFound)
}, },
} }
} }

View file

@ -1,25 +1,20 @@
use axum::{ use axum::{
extract::State, extract::State,
http::StatusCode,
response::{IntoResponse, Response},
Json Json
}; };
use tracing::debug; use tracing::debug;
use crate::{auth::{Token, Userinfo}, AppState}; use crate::{auth::{Token, Userinfo}, ApiResult, AppState};
pub(super) async fn create_user( pub(super) async fn create_user(
Token(token): Token, Token(token): Token,
State(state): State<AppState>, State(state): State<AppState>,
Json(json): Json<Userinfo> Json(json): Json<Userinfo>
) -> Response { ) -> ApiResult<&'static str> {
match state.config.lock().await.clone().verify_token(&token) { state.config.read().await.clone().verify_token(&token)?;
Ok(_) => {},
Err(e) => return e,
}
debug!("Creating new user: {json:?}"); debug!("Creating new user: {json:?}");
state.user_manager.insert_user(json.uuid, json); state.user_manager.insert_user(json.uuid, json);
(StatusCode::OK, "ok".to_string()).into_response() Ok("ok")
} }

View file

@ -1,20 +1,32 @@
use std::sync::Arc; use std::{sync::Arc, time::Duration};
use anyhow::anyhow; use anyhow::anyhow;
use axum::{ 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 dashmap::DashMap;
use tracing::{debug, trace}; use tracing::{debug, error, trace};
use uuid::Uuid; use uuid::Uuid;
use crate::AppState; use crate::{ApiError, ApiResult, AppState};
use super::types::*; use super::types::*;
const TIMEOUT: Duration = Duration::from_secs(5);
// It's an extractor that pulls a token from the Header. // It's an extractor that pulls a token from the Header.
#[derive(PartialEq, Debug)] #[derive(PartialEq, Debug)]
pub struct Token(pub Option<String>); 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] #[async_trait]
impl<S> FromRequestParts<S> for Token impl<S> FromRequestParts<S> for Token
@ -30,8 +42,8 @@ where
.and_then(|value| value.to_str().ok()); .and_then(|value| value.to_str().ok());
trace!(token = ?token); trace!(token = ?token);
match token { match token {
Some(token) => Ok(Self(Some(token.to_string()))), Some(token) => Ok(Self(token.to_string())),
None => Ok(Self(None)), None => Err(StatusCode::UNAUTHORIZED),
} }
} }
} }
@ -46,53 +58,89 @@ fn get_id_json(json: &serde_json::Value) -> anyhow::Result<Uuid> {
Ok(uuid) Ok(uuid)
} }
#[inline]
async fn fetch_json( async fn fetch_json(
auth_system: AuthSystem, auth_provider: &AuthProvider,
server_id: &str, server_id: &str,
username: &str, username: &str,
) -> anyhow::Result<Option<(Uuid, AuthSystem)>> { ) -> anyhow::Result<anyhow::Result<(Uuid, AuthProvider)>> {
let client = reqwest::Client::new(); let client = reqwest::Client::builder().timeout(TIMEOUT).build().unwrap();
let url = auth_system.get_url(); let url = auth_provider.url.clone();
let res = client let res = client
.get(url) .get(url)
.query(&[("serverId", server_id), ("username", username)]) .query(&[("serverId", server_id), ("username", username)])
.send() .send()
.await?; .await?;
debug!("{res:?}"); trace!("{res:?}");
match res.status().as_u16() { match res.status().as_u16() {
200 => { 200 => {
let json = serde_json::from_str::<serde_json::Value>(&res.text().await?)?; let json = serde_json::from_str::<serde_json::Value>(&res.text().await?)?;
let uuid = get_id_json(&json)?; let uuid = get_id_json(&json)?;
Ok(Some((uuid, auth_system))) Ok(Ok((uuid, auth_provider.clone())))
} }
401 => Ok(None), // Ely.By None _ => Ok(Err(anyhow!("notOK: {} data: {:?}", res.status().as_u16(), res.text().await))),
204 => Ok(None), // Mojang None
_ => Err(anyhow!("Unknown code: {}", res.status().as_u16())),
} }
} }
pub async fn has_joined( pub async fn has_joined(
AuthProviders(authproviders): AuthProviders,
server_id: &str, server_id: &str,
username: &str, username: &str,
) -> anyhow::Result<Option<(Uuid, AuthSystem)>> { ) -> anyhow::Result<Option<(Uuid, AuthProvider)>> {
let (elyby, mojang) = ( let (tx, mut rx) = tokio::sync::mpsc::channel(1);
fetch_json(AuthSystem::ElyBy,server_id, username).await?,
fetch_json(AuthSystem::Mojang, server_id, username).await? 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))
if elyby.is_none() && mojang.is_none() {
Ok(None)
} else if mojang.is_some() {
Ok(mojang)
} else if elyby.is_some() {
Ok(elyby)
} else { } 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<anyhow::Result<anyhow::Result<(Uuid, AuthProvider)>>>
) {
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 // User manager
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -100,7 +148,7 @@ pub struct UManager {
/// Users with incomplete authentication /// Users with incomplete authentication
pending: Arc<DashMap<String, String>>, // <SHA1 serverId, USERNAME> TODO: Add automatic purge pending: Arc<DashMap<String, String>>, // <SHA1 serverId, USERNAME> TODO: Add automatic purge
/// Authenticated users TODO: Change name to sessions /// Authenticated users TODO: Change name to sessions
authenticated: Arc<DashMap<String, Uuid>>, // <SHA1 serverId, Userinfo> NOTE: In the future, try it in a separate LockRw branch authenticated: Arc<DashMap<String, Uuid>>, // <SHA1 serverId, Userinfo>
/// Registered users /// Registered users
registered: Arc<DashMap<Uuid, Userinfo>>, registered: Arc<DashMap<Uuid, Userinfo>>,
} }
@ -116,15 +164,24 @@ impl UManager {
pub fn pending_insert(&self, server_id: String, username: String) { pub fn pending_insert(&self, server_id: String, username: String) {
self.pending.insert(server_id, username); 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) self.pending.remove(server_id)
} }
pub fn insert(&self, uuid: Uuid, token: String, userinfo: Userinfo) -> Option<Userinfo> { pub fn insert(&self, uuid: Uuid, token: String, userinfo: Userinfo) {
self.authenticated.insert(token, uuid); 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<Userinfo> { pub fn insert_user(&self, uuid: Uuid, userinfo: Userinfo) {
self.registered.insert(uuid, 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( pub fn get(
&self, &self,
@ -139,32 +196,46 @@ impl UManager {
) -> Option<dashmap::mapref::one::Ref<'_, Uuid, Userinfo>> { ) -> Option<dashmap::mapref::one::Ref<'_, Uuid, Userinfo>> {
self.registered.get(uuid) 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 { pub fn is_authenticated(&self, token: &String) -> bool {
self.authenticated.contains_key(token) self.authenticated.contains_key(token)
} }
pub fn _is_registered(&self, uuid: &Uuid) -> bool { pub fn _is_registered(&self, uuid: &Uuid) -> bool {
self.registered.contains_key(uuid) 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) { 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); self.authenticated.remove(&token);
} }
} }
// End of User manager // End of User manager
pub async fn check_auth( pub async fn check_auth(
Token(token): Token, token: Option<Token>,
State(state): State<AppState>, State(state): State<AppState>,
) -> Response { ) -> ApiResult<&'static str> {
match token { match token {
Some(token) => { Some(token) => {
if state.user_manager.is_authenticated(&token) { token.check_auth(&state).await?;
(StatusCode::OK, "ok".to_string()).into_response() Ok("ok")
} else {
(StatusCode::UNAUTHORIZED, "unauthorized".to_string()).into_response()
}
}, },
None => (StatusCode::BAD_REQUEST, "bad request".to_string()).into_response(), None => Err(ApiError::BadRequest),
} }
} }

View file

@ -1,56 +1,71 @@
use std::str::FromStr; use chrono::Utc;
use serde::Deserialize; use serde::Deserialize;
use uuid::Uuid; use uuid::Uuid;
use anyhow::anyhow;
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Userinfo { pub struct Userinfo {
pub username: String,
pub uuid: Uuid, pub uuid: Uuid,
pub auth_system: AuthSystem, pub username: String,
pub rank: String,
pub last_used: String,
pub auth_provider: AuthProvider,
pub token: Option<String>, pub token: Option<String>,
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)] #[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "camelCase")]
pub enum AuthSystem { pub struct AuthProviders(pub Vec<AuthProvider>);
Internal,
ElyBy, pub fn default_authproviders() -> AuthProviders {
Mojang, 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() }
])
} }
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<Self, Self::Err> {
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"),
}
}
}

View file

@ -1,29 +1,18 @@
use anyhow::Result; use anyhow::Result;
use axum::{ 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 dashmap::DashMap;
use tracing_panic::panic_hook;
use tracing_subscriber::{fmt::{self, time::ChronoLocal}, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::{broadcast, mpsc, Mutex}; use tokio::{sync::{broadcast, mpsc, RwLock}, time::Instant};
use tower_http::trace::TraceLayer; use tower_http::trace::TraceLayer;
use tracing::info; use tracing::info;
use uuid::Uuid; use uuid::Uuid;
// // WebSocket worker // Errors
// mod ws; pub use api::errors::{ApiResult, ApiError};
// 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;
// API // API
mod api; mod api;
@ -42,14 +31,12 @@ use state::Config;
// Utils // Utils
mod utils; mod utils;
use utils::{check_updates, update_advanced_users}; use utils::{check_updates, get_log_file, update_advanced_users, update_bans_from_minecraft};
// // Config
// mod config;
// use config::Config;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct AppState { pub struct AppState {
/// Uptime
uptime: Instant,
/// User manager /// User manager
user_manager: Arc<UManager>, user_manager: Arc<UManager>,
/// Send into WebSocket /// Send into WebSocket
@ -57,57 +44,91 @@ pub struct AppState {
/// Ping broadcasts for WebSocket connections /// Ping broadcasts for WebSocket connections
broadcasts: Arc<DashMap<Uuid, broadcast::Sender<Vec<u8>>>>, broadcasts: Arc<DashMap<Uuid, broadcast::Sender<Vec<u8>>>>,
/// Current configuration /// Current configuration
config: Arc<Mutex<state::Config>>, config: Arc<RwLock<state::Config>>,
} }
const LOGGER_ENV: &'static str = "RUST_LOG"; 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 SCULPTOR_VERSION: &'static str = env!("CARGO_PKG_VERSION");
const REPOSITORY: &'static str = "shiroyashik/sculptor";
#[tokio::main] #[tokio::main]
async fn main() -> Result<()> { async fn main() -> Result<()> {
let _ = dotenvy::dotenv(); let _ = dotenvy::dotenv();
// "trace,axum=info,tower_http=info,tokio=info,tungstenite=info,tokio_tungstenite=info", // "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 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() let file_appender = tracing_appender::rolling::never(&logs_folder, get_log_file(&logs_folder));
.with_env_filter( let timer = ChronoLocal::new(String::from("%Y-%m-%dT%H:%M:%S%.3f%:z"));
logger_env
) let file_layer = fmt::layer()
.with_ansi(false) // Disable ANSI colors for file logs
.with_timer(timer.clone())
.pretty() .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(); .init();
info!("The Sculptor v{}{}", SCULPTOR_VERSION, check_updates("shiroyashik/sculptor", &SCULPTOR_VERSION).await?); 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?);
let config_file = std::env::var("CONFIG_PATH").unwrap_or_else(|_| "Config.toml".into());
// Config // Config
let config = Arc::new(Mutex::new(Config::parse(config_file.clone().into()))); let config = Arc::new(RwLock::new(Config::parse(config_file.clone().into())));
let listen = config.lock().await.listen.clone(); let listen = config.read().await.listen.clone();
// State // State
let state = AppState { let state = AppState {
uptime: Instant::now(),
user_manager: Arc::new(UManager::new()), user_manager: Arc::new(UManager::new()),
session: Arc::new(DashMap::new()), session: Arc::new(DashMap::new()),
broadcasts: Arc::new(DashMap::new()), broadcasts: Arc::new(DashMap::new()),
config: config, config,
}; };
// Automatic update of configuration while the server is running // Automatic update of configuration while the server is running
let config_update = Arc::clone(&state.config); let config_update = Arc::clone(&state.config);
let user_manager = Arc::clone(&state.user_manager); 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 { tokio::spawn(async move {
loop { loop {
tokio::time::sleep(std::time::Duration::from_secs(10)).await; tokio::time::sleep(std::time::Duration::from_secs(10)).await;
let new_config = Config::parse(config_file.clone().into()); 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 { if new_config != *config {
info!("Server configuration modification detected!"); info!("Server configuration modification detected!");
*config = new_config; *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() let api = Router::new()
.nest("//auth", api_auth::router()) .nest("//auth", api_auth::router())
@ -126,7 +147,6 @@ async fn main() -> Result<()> {
.nest("/api", api) .nest("/api", api)
.route("/ws", get(ws)) .route("/ws", get(ws))
.route("/health", get(|| async { "ok" })) .route("/health", get(|| async { "ok" }))
.route_layer(from_extractor::<auth::Token>())
.with_state(state) .with_state(state)
.layer(TraceLayer::new_for_http().on_request(())); .layer(TraceLayer::new_for_http().on_request(()));
@ -155,8 +175,13 @@ async fn shutdown_signal() {
#[cfg(not(unix))] #[cfg(not(unix))]
let terminate = std::future::pending::<()>(); let terminate = std::future::pending::<()>();
tokio::select! { tokio::select! {
() = ctrl_c => {}, () = ctrl_c => {
() = terminate => {}, println!();
info!("Ctrl+C signal received");
},
() = terminate => {
println!();
info!("Terminate signal received");
},
} }
info!("Terminate signal received");
} }

View file

@ -1,37 +1,37 @@
use std::{io::Read, path::PathBuf}; use std::{collections::HashMap, io::Read, path::PathBuf};
use serde::Deserialize; 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)] #[derive(Deserialize, Clone, Debug, PartialEq)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Config { pub struct Config {
pub listen: String, pub listen: String,
pub token: Option<String>, pub token: Option<String>,
pub motd: String, pub motd: CMotd,
#[serde(default = "default_authproviders")]
pub auth_providers: AuthProviders,
pub limitations: Limitations, pub limitations: Limitations,
pub advanced_users: Table, #[serde(default)]
pub mc_folder: PathBuf,
#[serde(default)]
pub advanced_users: HashMap<Uuid, AdvancedUsers>,
} }
impl Config { #[derive(Deserialize, Clone, Debug, PartialEq)]
pub fn verify_token(&self, suspicious: &Option<String>) -> Result<axum::response::Response, axum::response::Response> { #[serde(rename_all = "camelCase")]
use axum::{http::StatusCode, response::IntoResponse}; pub struct CMotd {
match &self.token { pub display_server_info: bool,
Some(token) => { pub custom_text: String,
match suspicious { #[serde(rename = "sInfoUptime")]
Some(suspicious) => { pub text_uptime: String,
if token == suspicious { #[serde(rename = "sInfoAuthClients")]
return Ok((StatusCode::OK, "ok".to_string()).into_response()) pub text_authclients: String,
} else { #[serde(rename = "sInfoDrawIndent")]
return Err((StatusCode::UNAUTHORIZED, "wrong token".to_string()).into_response()) pub draw_indent: bool,
}
},
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)] #[derive(Deserialize, Clone, Debug, PartialEq)]
@ -41,6 +41,37 @@ pub struct Limitations {
pub max_avatars: u64, 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<Userinfo> for BannedPlayer {
fn into(self) -> Userinfo {
Userinfo {
uuid: self.uuid,
username: self.name,
banned: true,
..Default::default()
}
}
}
impl Config { impl Config {
pub fn parse(path: PathBuf) -> Self { pub fn parse(path: PathBuf) -> Self {
let mut file = std::fs::File::open(path).expect("Access denied or file doesn't exists!"); 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() 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)
},
}
}
} }

View file

@ -1,5 +1,7 @@
mod utils; mod utils;
mod check_updates; mod check_updates;
mod motd;
pub use utils::*; pub use utils::*;
pub use motd::*;
pub use check_updates::check_updates; pub use check_updates::check_updates;

69
src/utils/motd.rs Normal file
View file

@ -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<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub click_event: Option<ClickEvent>,
#[serde(skip_serializing_if = "Option::is_none")]
pub underlined: Option<bool>,
}
#[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<Motd> {
let motd_settings = &state.config.read().await.motd;
let custom: Result<Vec<Motd>, 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
}
}

View file

@ -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 base64::prelude::*;
use rand::{distributions::Alphanumeric, thread_rng, Rng}; use rand::{distributions::Alphanumeric, thread_rng, Rng};
use ring::digest::{self, digest}; use ring::digest::{self, digest};
use tokio::io::AsyncReadExt;
use tracing::{error, info};
use uuid::Uuid; use uuid::Uuid;
use chrono::prelude::*;
use crate::auth::{AuthSystem, UManager, Userinfo}; use crate::{auth::{UManager, Userinfo}, state::{AdvancedUsers, BannedPlayer}};
// Core functions // Core functions
pub fn rand() -> [u8; 50] { pub fn rand() -> [u8; 50] {
@ -27,50 +30,79 @@ pub fn _generate_hex_string(length: usize) -> String {
hex::encode(random_bytes) hex::encode(random_bytes)
} }
pub fn get_correct_array(value: &toml::Value) -> Vec<u8> { pub fn update_advanced_users(value: &std::collections::HashMap<Uuid, AdvancedUsers>, umanager: &UManager) {
// let res: Vec<u8>;
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) {
let users: Vec<(Uuid, Userinfo)> = value let users: Vec<(Uuid, Userinfo)> = value
.iter() .iter()
.map( |(uuid, userdata)| { .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(), uuid.clone(),
Userinfo { username, Userinfo {
uuid: Uuid::parse_str(uuid).unwrap(), uuid: uuid.clone(),
auth_system, username: userdata.username.clone(),
token: None banned: userdata.banned,
..Default::default()
} }
)}) )})
.collect(); .collect();
for (uuid, userinfo) in users { 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<UManager>) {
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<BannedPlayer> = serde_json::from_str(&data).expect("cant parse banned-players.json");
if !old_bans.is_empty() {
let names: Vec<String> = 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<BannedPlayer> = 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::<Vec<String>>().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::<Vec<String>>().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<String, std::io::Error>
Ok(hex_hash) 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;
}
}