mirror of
https://github.com/shiroyashik/sculptor.git
synced 2025-12-06 13:01:12 +03:00
+ 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:
parent
bd101fc3fa
commit
d45a495cbf
21 changed files with 748 additions and 378 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -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
108
Cargo.lock
generated
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
|
|
||||||
|
|
|
||||||
6
note.txt
6
note.txt
|
|
@ -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
41
src/api/errors.rs
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,3 @@
|
||||||
pub mod figura;
|
pub mod figura;
|
||||||
pub mod v1;
|
pub mod v1;
|
||||||
|
pub mod errors;
|
||||||
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
@ -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)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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")
|
||||||
}
|
}
|
||||||
161
src/auth/auth.rs
161
src/auth/auth.rs
|
|
@ -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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
||||||
103
src/main.rs
103
src/main.rs
|
|
@ -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");
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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
69
src/utils/motd.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue